mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 13:51:10 -05:00
Compare commits
431 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27ab7eb2f0 | ||
|
|
6d183be0ec | ||
|
|
c83a34b473 | ||
|
|
d3e0e597d2 | ||
|
|
271bbba7dd | ||
|
|
86ff2e1e6b | ||
|
|
6ef7cc0ccb | ||
|
|
f4cf32e768 | ||
|
|
47384c1f18 | ||
|
|
9e3d533acc | ||
|
|
5ec4f65ac3 | ||
|
|
4a330ae36f | ||
|
|
d93fbc0f6b | ||
|
|
684bb78897 | ||
|
|
d0dd16c527 | ||
|
|
f4805343f8 | ||
|
|
f9add3e936 | ||
|
|
1fd9656291 | ||
|
|
6f281a6401 | ||
|
|
5194d2b3cd | ||
|
|
b3c23c5f88 | ||
|
|
a15134cc2f | ||
|
|
b51bb101f2 | ||
|
|
59d26dbbe7 | ||
|
|
94baf19e6a | ||
|
|
f1a446fb02 | ||
|
|
01d1242753 | ||
|
|
203685e91c | ||
|
|
2cb5b85ab2 | ||
|
|
ec996d3509 | ||
|
|
5ed3223185 | ||
|
|
3bdcc0a5a6 | ||
|
|
1e4d4887e1 | ||
|
|
94b922d27a | ||
|
|
0a8f9a3f6b | ||
|
|
a5d4d1e0ad | ||
|
|
2bdb5866a7 | ||
|
|
65932289f7 | ||
|
|
e34f9c6408 | ||
|
|
e0e13e375e | ||
|
|
4c333bee95 | ||
|
|
965f7bde0d | ||
|
|
cfcba01d03 | ||
|
|
3dc40f1742 | ||
|
|
5a366f5707 | ||
|
|
b960c697dc | ||
|
|
0383b26f46 | ||
|
|
8884008b4d | ||
|
|
af3012b0f2 | ||
|
|
3d6caa504f | ||
|
|
87213edf39 | ||
|
|
4f081bb5ce | ||
|
|
78d356d2ed | ||
|
|
64dc7f41d8 | ||
|
|
cb0a992d8d | ||
|
|
c2eb375f69 | ||
|
|
a555c6b6b2 | ||
|
|
0752fa4e43 | ||
|
|
f14fc61b4b | ||
|
|
89878519b4 | ||
|
|
46d57bcf3c | ||
|
|
f3e85da9a7 | ||
|
|
83444650a3 | ||
|
|
dae7490739 | ||
|
|
5bc87d5c17 | ||
|
|
7a58c415de | ||
|
|
09576d7d57 | ||
|
|
0bfcd3c9ce | ||
|
|
bf3de7a80d | ||
|
|
7269c372d6 | ||
|
|
8308f15e93 | ||
|
|
2420919ca8 | ||
|
|
c671c2e904 | ||
|
|
c9df8dd1f3 | ||
|
|
8ae86d4310 | ||
|
|
c144c26dcf | ||
|
|
481f6b1531 | ||
|
|
b267d56ba7 | ||
|
|
dd139f8228 | ||
|
|
be4756e4bf | ||
|
|
bd323fb930 | ||
|
|
aff1070f43 | ||
|
|
b6742e90b1 | ||
|
|
95b6ed7965 | ||
|
|
5a35144d4f | ||
|
|
5f5f9d6fd5 | ||
|
|
c602a2fcd6 | ||
|
|
821d3c423c | ||
|
|
6b89f5dad8 | ||
|
|
d28cf53009 | ||
|
|
504900d7bd | ||
|
|
0cc439eee3 | ||
|
|
e8b8f34f2b | ||
|
|
58a23e06a1 | ||
|
|
5a4ac03c0d | ||
|
|
c5a506e240 | ||
|
|
bc5cd13c16 | ||
|
|
d406c7c09b | ||
|
|
9cf3117636 | ||
|
|
61786ddcdf | ||
|
|
f33281fae2 | ||
|
|
93bd457a54 | ||
|
|
600824bed2 | ||
|
|
42e635bb9e | ||
|
|
d48810f045 | ||
|
|
3387cf373d | ||
|
|
7825bd6c5a | ||
|
|
ba14f4ad53 | ||
|
|
02bbc3f777 | ||
|
|
45a2d0b40e | ||
|
|
53eccf7047 | ||
|
|
63000bc122 | ||
|
|
220cb4f0c8 | ||
|
|
7ce3a41184 | ||
|
|
51efaf2e81 | ||
|
|
f0d6d428dc | ||
|
|
01778fc90a | ||
|
|
d9928ad3f9 | ||
|
|
77b7876986 | ||
|
|
745f7c7313 | ||
|
|
0a96d0e3fa | ||
|
|
e3ad9d097e | ||
|
|
573b6522f9 | ||
|
|
59d3717159 | ||
|
|
a42eafc316 | ||
|
|
11f839b9e5 | ||
|
|
721274239a | ||
|
|
090eba0db5 | ||
|
|
88be4ac417 | ||
|
|
c1d622e56e | ||
|
|
91af1c3b54 | ||
|
|
4b6813ecee | ||
|
|
32687e64fe | ||
|
|
b7f46213c7 | ||
|
|
aa8982bdb4 | ||
|
|
14826db606 | ||
|
|
847a582022 | ||
|
|
97cd00faeb | ||
|
|
8d4193fb46 | ||
|
|
8d4ae388fa | ||
|
|
7082689866 | ||
|
|
40c034ed3f | ||
|
|
208d5ea7ab | ||
|
|
ed51438c8b | ||
|
|
49fce6673a | ||
|
|
a016ac0d2b | ||
|
|
ad55f5367e | ||
|
|
660cbfa6ce | ||
|
|
982608246e | ||
|
|
6f6de2cf05 | ||
|
|
ae3e3f8269 | ||
|
|
dc21f1497b | ||
|
|
5c8941ba16 | ||
|
|
b0a001c6a4 | ||
|
|
722c55f85f | ||
|
|
3ab19f3295 | ||
|
|
621ffc46c0 | ||
|
|
fbb629c02e | ||
|
|
80d3a86468 | ||
|
|
8e8fd2717e | ||
|
|
f92d24e89c | ||
|
|
94e0a44e15 | ||
|
|
f25181f68d | ||
|
|
cf19eb1386 | ||
|
|
0583119698 | ||
|
|
3389f3db7f | ||
|
|
c642f5f8a3 | ||
|
|
ceb8b509e2 | ||
|
|
65dae11e47 | ||
|
|
d5b1676216 | ||
|
|
09f222baf7 | ||
|
|
802e7bffc3 | ||
|
|
fb272d1afa | ||
|
|
b871676def | ||
|
|
869fe2f50d | ||
|
|
db877b1c9b | ||
|
|
4749f9d97a | ||
|
|
8564a7fb51 | ||
|
|
6be98e0bbd | ||
|
|
5e0c68c45e | ||
|
|
7717b9dcf2 | ||
|
|
819a108f07 | ||
|
|
3a99899114 | ||
|
|
1088066c69 | ||
|
|
73d44503fd | ||
|
|
25aaff0bd6 | ||
|
|
29691461c5 | ||
|
|
a74e4dcec8 | ||
|
|
0b0b548d7a | ||
|
|
02f3823e4c | ||
|
|
36327efe9d | ||
|
|
079f52a191 | ||
|
|
c9f1d01346 | ||
|
|
9bc68bdd93 | ||
|
|
56c225bd34 | ||
|
|
55c017cad1 | ||
|
|
7849d55b99 | ||
|
|
c903cc8827 | ||
|
|
4a797b9565 | ||
|
|
57b487eaa3 | ||
|
|
891b7c06f2 | ||
|
|
f7e212ec9c | ||
|
|
228538ea62 | ||
|
|
347e7886e1 | ||
|
|
bfa5fb8844 | ||
|
|
a8fd6038f1 | ||
|
|
fbfe887084 | ||
|
|
aed991f75f | ||
|
|
ab2cb71c00 | ||
|
|
fcfde083e7 | ||
|
|
4614a3637a | ||
|
|
d450544d6b | ||
|
|
9340e6a78d | ||
|
|
e00b8835cc | ||
|
|
429ca06289 | ||
|
|
0fbfc90bea | ||
|
|
581e5025a2 | ||
|
|
e57fce290b | ||
|
|
d5a3682de9 | ||
|
|
75f5862218 | ||
|
|
aed518f945 | ||
|
|
e942d41c1d | ||
|
|
38fcfd8732 | ||
|
|
82ec68128d | ||
|
|
9497354cfd | ||
|
|
baa936439d | ||
|
|
394061d28d | ||
|
|
0f25db67dc | ||
|
|
49ba91961f | ||
|
|
28ce772ae9 | ||
|
|
424c2d9f6b | ||
|
|
89797064b8 | ||
|
|
7824aed878 | ||
|
|
8236c43c90 | ||
|
|
6df89fbe94 | ||
|
|
b423bf7ddd | ||
|
|
cdbdb8248c | ||
|
|
db45052d7e | ||
|
|
804bbf6564 | ||
|
|
bd3b9bacf6 | ||
|
|
5505d57066 | ||
|
|
cf88c4e720 | ||
|
|
9911e86039 | ||
|
|
9eba0f6ab2 | ||
|
|
6d265c1cce | ||
|
|
904aa530b5 | ||
|
|
fa316f3edc | ||
|
|
41d19e7338 | ||
|
|
4f15a3f85c | ||
|
|
222166ca5a | ||
|
|
ab3eb81e52 | ||
|
|
f37486a74f | ||
|
|
a38b854338 | ||
|
|
e18113a746 | ||
|
|
d4edbd1acf | ||
|
|
056e45a003 | ||
|
|
72b3227824 | ||
|
|
a51f8a6b8e | ||
|
|
1ef8d70ac4 | ||
|
|
a204946d39 | ||
|
|
3c7795cf44 | ||
|
|
9349204636 | ||
|
|
d2bcd04133 | ||
|
|
61708ad0bd | ||
|
|
c9cfe22fd6 | ||
|
|
5572d3d4de | ||
|
|
1441fe0b91 | ||
|
|
7df1d8c838 | ||
|
|
3a51abfcad | ||
|
|
870e9086dc | ||
|
|
1db52ff531 | ||
|
|
e3be293420 | ||
|
|
bbc32dff82 | ||
|
|
861197e61c | ||
|
|
34e5ef22a0 | ||
|
|
b141297651 | ||
|
|
0cb977e603 | ||
|
|
c8a5adcee9 | ||
|
|
ecdb411711 | ||
|
|
a4e206168d | ||
|
|
4f1b5eae54 | ||
|
|
54f14e89cf | ||
|
|
1b3922d466 | ||
|
|
00a30a9984 | ||
|
|
db6699dae2 | ||
|
|
4d229d7b94 | ||
|
|
d94c5f8380 | ||
|
|
099217390e | ||
|
|
4c4ab8a57d | ||
|
|
d746b23749 | ||
|
|
f77c597e01 | ||
|
|
3c54a4d33b | ||
|
|
cf9de82c2a | ||
|
|
c3155e2642 | ||
|
|
d8f171a996 | ||
|
|
0ef3bb1cc6 | ||
|
|
54df8234ed | ||
|
|
dc08e37320 | ||
|
|
45a8575b95 | ||
|
|
be966cfe1f | ||
|
|
f4507aeb9b | ||
|
|
0985691fbd | ||
|
|
01047846a9 | ||
|
|
75a6d65e5a | ||
|
|
71ea92bbec | ||
|
|
aac6de01de | ||
|
|
af036d9f45 | ||
|
|
04016f3be6 | ||
|
|
41567b0456 | ||
|
|
9db3a601ca | ||
|
|
35fdd00203 | ||
|
|
7a435be018 | ||
|
|
dec0e24275 | ||
|
|
c624fc078b | ||
|
|
b9c6b326b7 | ||
|
|
0003072623 | ||
|
|
bffdb0b03d | ||
|
|
b5ffc734a2 | ||
|
|
a2c88ae0f1 | ||
|
|
efb671aaf2 | ||
|
|
a4b4b8f649 | ||
|
|
4c59405e5c | ||
|
|
703a215ef8 | ||
|
|
f5f96bc4f4 | ||
|
|
1668ab7877 | ||
|
|
26fc0da9a9 | ||
|
|
c15220d1a1 | ||
|
|
7c4562a8b3 | ||
|
|
6e3176f726 | ||
|
|
958b456873 | ||
|
|
a43b5b69ab | ||
|
|
1517435441 | ||
|
|
7abb28cb2e | ||
|
|
112fd4aac3 | ||
|
|
90fbe112af | ||
|
|
c150529c7c | ||
|
|
fa6aa2ced8 | ||
|
|
39664985ba | ||
|
|
ab1e9694bc | ||
|
|
2c710d3b7d | ||
|
|
581ab2c945 | ||
|
|
274b48c82f | ||
|
|
e352e4f59c | ||
|
|
734936d819 | ||
|
|
0e1384b4d2 | ||
|
|
2160613c6a | ||
|
|
69bb5c7fba | ||
|
|
f32e1a7773 | ||
|
|
703c2f214b | ||
|
|
6de831778a | ||
|
|
ca46086e79 | ||
|
|
0079184c16 | ||
|
|
dcc9efea0a | ||
|
|
a3b508fab9 | ||
|
|
5359b487f2 | ||
|
|
c2d973997a | ||
|
|
b09aa0e65c | ||
|
|
41a6f0d43e | ||
|
|
9764f8886b | ||
|
|
1ba2c063e0 | ||
|
|
c640294dbf | ||
|
|
dec487c62b | ||
|
|
1ba74a30fc | ||
|
|
fcf0cebf6c | ||
|
|
e14d38194f | ||
|
|
294aad092e | ||
|
|
8767ebf835 | ||
|
|
cd907f2edf | ||
|
|
eb77083d23 | ||
|
|
219362318c | ||
|
|
68a75dae48 | ||
|
|
87a381e0a7 | ||
|
|
0b2520f84a | ||
|
|
21ab85cd03 | ||
|
|
486bf32ac7 | ||
|
|
4f6610716a | ||
|
|
6db4ca71da | ||
|
|
d5319e2b4f | ||
|
|
cda44480b7 | ||
|
|
fb0af12433 | ||
|
|
b5f858da4e | ||
|
|
59bd5db556 | ||
|
|
cf1557e454 | ||
|
|
36e1f01091 | ||
|
|
e3c484af01 | ||
|
|
4deb5c7cee | ||
|
|
21fb329d56 | ||
|
|
678b469835 | ||
|
|
ded48ddac7 | ||
|
|
8a02fc755f | ||
|
|
4844f72b96 | ||
|
|
f32bd6f945 | ||
|
|
f64fcbc07d | ||
|
|
c34c3bf730 | ||
|
|
de4c551363 | ||
|
|
d45f02d38c | ||
|
|
666975a1ef | ||
|
|
144a1e4009 | ||
|
|
8b486dfc71 | ||
|
|
db092a404e | ||
|
|
edda3d1b51 | ||
|
|
27a11e7b35 | ||
|
|
cfd4c93d88 | ||
|
|
b1ca805d2a | ||
|
|
852a882fab | ||
|
|
fb0cbc90e3 | ||
|
|
3a24f10d7c | ||
|
|
3fc036e01a | ||
|
|
056a46cc97 | ||
|
|
f8df8ed72d | ||
|
|
79c159d123 | ||
|
|
a8c37ceace | ||
|
|
cb01f11ad1 | ||
|
|
7aaa84853d | ||
|
|
75857fbf73 | ||
|
|
c8db0c8ec6 | ||
|
|
3958260353 | ||
|
|
8cdb8d0367 | ||
|
|
66bf85b14f | ||
|
|
1a0892745e | ||
|
|
76b0493fb0 | ||
|
|
74eb4059d6 | ||
|
|
13f53eb64f | ||
|
|
b3941526cb | ||
|
|
fff067b2a8 | ||
|
|
217546c2a0 | ||
|
|
40c06f5e77 | ||
|
|
bb09caa9a3 | ||
|
|
4ebefeb43a | ||
|
|
8f01d0234f | ||
|
|
13035baeae |
45
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
labels: ["C-bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for filing a 🐛 bug report 😄!
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: >
|
||||
Please provide a clear and concise description of what the bug is,
|
||||
including what currently happens and what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps
|
||||
description: Please list the steps to reproduce the bug.
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
- type: textarea
|
||||
id: possible-solutions
|
||||
attributes:
|
||||
label: Possible Solution(s)
|
||||
description: >
|
||||
Not obligatory, but suggest a fix/reason for the bug,
|
||||
or ideas how to implement the addition or change.
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
description: Provide any additional notes that might be helpful.
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: >
|
||||
Please paste the output of running `mdbook --version` or which version
|
||||
of the library you are using.
|
||||
render: text
|
||||
28
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
28
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Enhancement
|
||||
description: Suggest an idea for enhancing mdBook
|
||||
labels: ["C-enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for filing a 🙋 feature request 😄!
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: >
|
||||
Please provide a clear description of your use case and the problem
|
||||
this feature request is trying to solve.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: >
|
||||
Please provide a clear and concise description of what you want to happen.
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
description: Provide any additional context or information that might be helpful.
|
||||
24
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Question
|
||||
description: Have a question on how to use mdBook?
|
||||
labels: ["C-question"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Got a question on how to do something with mdBook?
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question
|
||||
description: >
|
||||
Enter your question here. Please try to provide as much detail as possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: >
|
||||
Please paste the output of running `mdbook --version` or which version
|
||||
of the library you are using.
|
||||
render: text
|
||||
49
.github/workflows/deploy.yml
vendored
49
.github/workflows/deploy.yml
vendored
@@ -3,31 +3,45 @@ on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Deploy Release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
include:
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-20.04
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
name: Deploy ${{ matrix.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install hub
|
||||
run: ci/install-hub.sh ${{ matrix.os }}
|
||||
shell: bash
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: ci/install-rust.sh stable
|
||||
shell: bash
|
||||
- name: Build and deploy artifacts
|
||||
run: ci/install-rust.sh stable ${{ matrix.target }}
|
||||
- name: Build asset
|
||||
run: ci/make-release-asset.sh ${{ matrix.os }} ${{ matrix.target }}
|
||||
- name: Update release with new asset
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ci/make-release.sh ${{ matrix.os }}
|
||||
shell: bash
|
||||
run: gh release upload $MDBOOK_TAG $MDBOOK_ASSET
|
||||
pages:
|
||||
name: GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Build book
|
||||
@@ -40,3 +54,14 @@ jobs:
|
||||
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
|
||||
cd guide/book
|
||||
/tmp/deploy
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo publish --no-verify
|
||||
|
||||
76
.github/workflows/main.yml
vendored
76
.github/workflows/main.yml
vendored
@@ -1,52 +1,88 @@
|
||||
name: CI
|
||||
on:
|
||||
# Only run when merging to master, or open/synchronize/reopen a PR.
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
build: [stable, beta, nightly, macos, windows, msrv]
|
||||
include:
|
||||
- build: stable
|
||||
- name: stable linux
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
- build: beta
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: beta linux
|
||||
os: ubuntu-latest
|
||||
rust: beta
|
||||
- build: nightly
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: nightly linux
|
||||
os: ubuntu-latest
|
||||
rust: nightly
|
||||
- build: macos
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: stable x86_64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-musl
|
||||
- name: stable x86_64 macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
- build: windows
|
||||
target: x86_64-apple-darwin
|
||||
- name: stable aarch64 macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
target: aarch64-apple-darwin
|
||||
- name: stable windows-msvc
|
||||
os: windows-latest
|
||||
rust: stable
|
||||
- build: msrv
|
||||
os: ubuntu-latest
|
||||
# sync MSRV with docs: guide/src/guide/installation.md
|
||||
rust: 1.54.0
|
||||
target: x86_64-pc-windows-msvc
|
||||
- name: msrv
|
||||
os: ubuntu-20.04
|
||||
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
|
||||
rust: 1.74.0
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: ${{ matrix.name }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh ${{ matrix.rust }}
|
||||
run: bash ci/install-rust.sh ${{ matrix.rust }} ${{ matrix.target }}
|
||||
- name: Build and run tests
|
||||
run: cargo test
|
||||
run: cargo test --locked --target ${{ matrix.target }}
|
||||
- name: Test no default
|
||||
run: cargo test --no-default-features
|
||||
run: cargo test --no-default-features --target ${{ matrix.target }}
|
||||
|
||||
aarch64-cross-builds:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable aarch64-unknown-linux-musl
|
||||
- name: Build
|
||||
run: cargo build --locked --target aarch64-unknown-linux-musl
|
||||
|
||||
rustfmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt --check
|
||||
|
||||
# The success job is here to consolidate the total success/failure state of
|
||||
# all other jobs. This job is then included in the GitHub branch protection
|
||||
# rule which prevents merges unless all other jobs are passing. This makes
|
||||
# it easier to manage the list of jobs via this yml file and to prevent
|
||||
# accidentally adding new jobs without also updating the branch protections.
|
||||
success:
|
||||
name: Success gate
|
||||
if: always()
|
||||
needs:
|
||||
- test
|
||||
- rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'
|
||||
- name: Done
|
||||
run: exit 0
|
||||
|
||||
339
CHANGELOG.md
339
CHANGELOG.md
@@ -1,5 +1,342 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.4.41
|
||||
[v0.4.40...v0.4.41](https://github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)
|
||||
|
||||
### Added
|
||||
|
||||
- Added preliminary support for Rust 2024 edition.
|
||||
[#2398](https://github.com/rust-lang/mdBook/pull/2398)
|
||||
- Added a full example of the remove-emphasis preprocessor.
|
||||
[#2464](https://github.com/rust-lang/mdBook/pull/2464)
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted styling of clipboard/play icons.
|
||||
[#2421](https://github.com/rust-lang/mdBook/pull/2421)
|
||||
- Updated to handlebars v6.
|
||||
[#2416](https://github.com/rust-lang/mdBook/pull/2416)
|
||||
- Attr and section rules now have specific code highlighting.
|
||||
[#2448](https://github.com/rust-lang/mdBook/pull/2448)
|
||||
- The sidebar is now loaded from a common file, significantly reducing the book size when there are many chapters.
|
||||
[#2414](https://github.com/rust-lang/mdBook/pull/2414)
|
||||
- Updated dependencies.
|
||||
[#2470](https://github.com/rust-lang/mdBook/pull/2470)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved theme support when JavaScript is disabled.
|
||||
[#2454](https://github.com/rust-lang/mdBook/pull/2454)
|
||||
- Fixed broken themes when localStorage has an invalid theme id.
|
||||
[#2463](https://github.com/rust-lang/mdBook/pull/2463)
|
||||
- Adjusted the line-height of superscripts (and footnotes) to avoid adding extra space between lines.
|
||||
[#2465](https://github.com/rust-lang/mdBook/pull/2465)
|
||||
|
||||
## mdBook 0.4.40
|
||||
[v0.4.39...v0.4.40](https://github.com/rust-lang/mdBook/compare/v0.4.39...v0.4.40)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reverted the update to pulldown-cmark which broke the semver API.
|
||||
[#2388](https://github.com/rust-lang/mdBook/pull/2388)
|
||||
|
||||
## mdBook 0.4.39
|
||||
[v0.4.38...v0.4.39](https://github.com/rust-lang/mdBook/compare/v0.4.38...v0.4.39)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the automatic deploy broken in the previous release.
|
||||
[#2383](https://github.com/rust-lang/mdBook/pull/2383)
|
||||
|
||||
## mdBook 0.4.38
|
||||
[v0.4.37...v0.4.38](https://github.com/rust-lang/mdBook/compare/v0.4.37...v0.4.38)
|
||||
|
||||
### Added
|
||||
|
||||
- Added `nix` to the default set of languages supported for syntax highlighting.
|
||||
[#2262](https://github.com/rust-lang/mdBook/pull/2262)
|
||||
|
||||
### Changed
|
||||
|
||||
- The `output.html.curly-quotes` option has been renamed to `output.html.smart-punctuation` to better reflect what it does. The old option `curly-quotes` is kept for compatibility, but may be removed in the future.
|
||||
[#2327](https://github.com/rust-lang/mdBook/pull/2327)
|
||||
- The file-watcher used in `mdbook serve` and `mdbook watch` now uses a poll-based watcher instead of the native operating system notifications. This should fix issues on various systems and environments, and more accurately detect when files change. The native watcher can still be used with the `--watcher native` CLI option.
|
||||
[#2325](https://github.com/rust-lang/mdBook/pull/2325)
|
||||
- `mdbook test` output now includes color, and shows relative paths to the source.
|
||||
[#2259](https://github.com/rust-lang/mdBook/pull/2259)
|
||||
- Updated dependencies, MSRV raised to 1.74
|
||||
[#2350](https://github.com/rust-lang/mdBook/pull/2350)
|
||||
[#2351](https://github.com/rust-lang/mdBook/pull/2351)
|
||||
[#2378](https://github.com/rust-lang/mdBook/pull/2378)
|
||||
[#2381](https://github.com/rust-lang/mdBook/pull/2381)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reduced memory allocation when copying files.
|
||||
[#2355](https://github.com/rust-lang/mdBook/pull/2355)
|
||||
- Fixed the horizontal divider in `SUMMARY.md` from being indented into the previous nested section.
|
||||
[#2364](https://github.com/rust-lang/mdBook/pull/2364)
|
||||
- Removed unnecessary `@import` in the CSS.
|
||||
[#2260](https://github.com/rust-lang/mdBook/pull/2260)
|
||||
|
||||
## mdBook 0.4.37
|
||||
[v0.4.36...v0.4.37](https://github.com/rust-lang/mdBook/compare/v0.4.36...v0.4.37)
|
||||
|
||||
### Changed
|
||||
- ❗️ Updated the markdown parser. This brings in many changes to more closely follow the CommonMark spec. This may cause some small rendering changes. It is recommended to compare the output of the old and new version to check for changes. See <https://github.com/raphlinus/pulldown-cmark/releases/tag/v0.10.0> for more information.
|
||||
[#2308](https://github.com/rust-lang/mdBook/pull/2308)
|
||||
- The warning about the legacy `src/theme` directory has been removed.
|
||||
[#2263](https://github.com/rust-lang/mdBook/pull/2263)
|
||||
- Updated dependencies. MSRV raised to 1.71.0.
|
||||
[#2283](https://github.com/rust-lang/mdBook/pull/2283)
|
||||
[#2293](https://github.com/rust-lang/mdBook/pull/2293)
|
||||
[#2297](https://github.com/rust-lang/mdBook/pull/2297)
|
||||
[#2310](https://github.com/rust-lang/mdBook/pull/2310)
|
||||
[#2309](https://github.com/rust-lang/mdBook/pull/2309)
|
||||
- Some internal performance/memory improvements.
|
||||
[#2273](https://github.com/rust-lang/mdBook/pull/2273)
|
||||
[#2290](https://github.com/rust-lang/mdBook/pull/2290)
|
||||
- Made the `pathdiff` dependency optional based on the `watch` feature.
|
||||
[#2291](https://github.com/rust-lang/mdBook/pull/2291)
|
||||
|
||||
### Fixed
|
||||
- The `s` shortcut key handler should not trigger when focus is in an HTML form.
|
||||
[#2311](https://github.com/rust-lang/mdBook/pull/2311)
|
||||
|
||||
## mdBook 0.4.36
|
||||
[v0.4.35...v0.4.36](https://github.com/rust-lang/mdBook/compare/v0.4.35...v0.4.36)
|
||||
|
||||
### Added
|
||||
- Added Nim to the default highlighted languages.
|
||||
[#2232](https://github.com/rust-lang/mdBook/pull/2232)
|
||||
- Added a small indicator for the sidebar resize handle.
|
||||
[#2209](https://github.com/rust-lang/mdBook/pull/2209)
|
||||
|
||||
### Changed
|
||||
- Updated dependencies. MSRV raised to 1.70.0.
|
||||
[#2173](https://github.com/rust-lang/mdBook/pull/2173)
|
||||
[#2250](https://github.com/rust-lang/mdBook/pull/2250)
|
||||
[#2252](https://github.com/rust-lang/mdBook/pull/2252)
|
||||
|
||||
### Fixed
|
||||
- Fixed blank column in print page when the sidebar was visible.
|
||||
[#2235](https://github.com/rust-lang/mdBook/pull/2235)
|
||||
- Fixed indentation of code blocks when Javascript is disabled.
|
||||
[#2162](https://github.com/rust-lang/mdBook/pull/2162)
|
||||
- Fixed a panic when `mdbook serve` or `mdbook watch` were given certain kinds of paths.
|
||||
[#2229](https://github.com/rust-lang/mdBook/pull/2229)
|
||||
|
||||
## mdBook 0.4.35
|
||||
[v0.4.34...v0.4.35](https://github.com/rust-lang/mdBook/compare/v0.4.34...v0.4.35)
|
||||
|
||||
### Added
|
||||
- Added the `book.text-direction` setting for explicit support for right-to-left languages.
|
||||
[#1641](https://github.com/rust-lang/mdBook/pull/1641)
|
||||
- Added `rel=prefetch` to the "next" links to potentially improve browser performance.
|
||||
[#2168](https://github.com/rust-lang/mdBook/pull/2168)
|
||||
- Added a `.warning` CSS class which is styled for displaying warning blocks.
|
||||
[#2187](https://github.com/rust-lang/mdBook/pull/2187)
|
||||
|
||||
### Changed
|
||||
- Better support of the sidebar when JavaScript is disabled.
|
||||
[#2175](https://github.com/rust-lang/mdBook/pull/2175)
|
||||
|
||||
## mdBook 0.4.34
|
||||
[v0.4.33...v0.4.34](https://github.com/rust-lang/mdBook/compare/v0.4.33...v0.4.34)
|
||||
|
||||
### Fixed
|
||||
- Fixed file change watcher failing on macOS with a large number of files.
|
||||
[#2157](https://github.com/rust-lang/mdBook/pull/2157)
|
||||
|
||||
## mdBook 0.4.33
|
||||
[v0.4.32...v0.4.33](https://github.com/rust-lang/mdBook/compare/v0.4.32...v0.4.33)
|
||||
|
||||
### Added
|
||||
- The `color-scheme` CSS property is now set based on the light/dark theme, which applies some slight color differences in browser elements like scroll bars on some browsers.
|
||||
[#2134](https://github.com/rust-lang/mdBook/pull/2134)
|
||||
|
||||
### Fixed
|
||||
- Fixed watching of extra-watch-dirs when not running in the book root directory.
|
||||
[#2146](https://github.com/rust-lang/mdBook/pull/2146)
|
||||
- Reverted the dependency update to the `toml` crate (again!). This was an unintentional breaking change in 0.4.32.
|
||||
[#2021](https://github.com/rust-lang/mdBook/pull/2021)
|
||||
- Changed macOS change notifications to use the kqueue implementation which should fix some issues with repeated rebuilds when a file changed.
|
||||
[#2152](https://github.com/rust-lang/mdBook/pull/2152)
|
||||
- Don't set a background color in the print page for code blocks in a header.
|
||||
[#2150](https://github.com/rust-lang/mdBook/pull/2150)
|
||||
|
||||
## mdBook 0.4.32
|
||||
[v0.4.31...v0.4.32](https://github.com/rust-lang/mdBook/compare/v0.4.31...v0.4.32)
|
||||
|
||||
### Fixed
|
||||
- Fixed theme-color meta tag not syncing with the theme.
|
||||
[#2118](https://github.com/rust-lang/mdBook/pull/2118)
|
||||
|
||||
### Changed
|
||||
- Updated all dependencies.
|
||||
[#2121](https://github.com/rust-lang/mdBook/pull/2121)
|
||||
[#2122](https://github.com/rust-lang/mdBook/pull/2122)
|
||||
[#2123](https://github.com/rust-lang/mdBook/pull/2123)
|
||||
[#2124](https://github.com/rust-lang/mdBook/pull/2124)
|
||||
[#2125](https://github.com/rust-lang/mdBook/pull/2125)
|
||||
[#2126](https://github.com/rust-lang/mdBook/pull/2126)
|
||||
|
||||
## mdBook 0.4.31
|
||||
[v0.4.30...v0.4.31](https://github.com/rust-lang/mdBook/compare/v0.4.30...v0.4.31)
|
||||
|
||||
### Fixed
|
||||
- Fixed menu border render flash during page navigation.
|
||||
[#2101](https://github.com/rust-lang/mdBook/pull/2101)
|
||||
- Fixed flicker setting sidebar scroll position.
|
||||
[#2104](https://github.com/rust-lang/mdBook/pull/2104)
|
||||
- Fixed compile error with proc-macro2 on latest Rust nightly.
|
||||
[#2109](https://github.com/rust-lang/mdBook/pull/2109)
|
||||
|
||||
## mdBook 0.4.30
|
||||
[v0.4.29...v0.4.30](https://github.com/rust-lang/mdBook/compare/v0.4.29...v0.4.30)
|
||||
|
||||
### Added
|
||||
- Added support for heading attributes.
|
||||
Attributes are specified in curly braces just after the heading text.
|
||||
An HTML ID can be specified with `#` and classes with `.`.
|
||||
For example: `## My heading {#custom-id .class1 .class2}`
|
||||
[#2013](https://github.com/rust-lang/mdBook/pull/2013)
|
||||
- Added support for hidden code lines for languages other than Rust.
|
||||
The `output.html.code.hidelines` table allows you to define the prefix character that will be used to hide code lines based on the language.
|
||||
[#2093](https://github.com/rust-lang/mdBook/pull/2093)
|
||||
|
||||
### Fixed
|
||||
- Fixed a few minor markdown rendering issues.
|
||||
[#2092](https://github.com/rust-lang/mdBook/pull/2092)
|
||||
|
||||
## mdBook 0.4.29
|
||||
[v0.4.28...v0.4.29](https://github.com/rust-lang/mdBook/compare/v0.4.28...v0.4.29)
|
||||
|
||||
### Changed
|
||||
- Built-in fonts are no longer copied when `fonts/fonts.css` is overridden in the theme directory.
|
||||
Additionally, the warning about `copy-fonts` has been removed if `fonts/fonts.css` is specified.
|
||||
[#2080](https://github.com/rust-lang/mdBook/pull/2080)
|
||||
- `mdbook init --force` now skips all interactive prompts as intended.
|
||||
[#2057](https://github.com/rust-lang/mdBook/pull/2057)
|
||||
- Updated dependencies
|
||||
[#2063](https://github.com/rust-lang/mdBook/pull/2063)
|
||||
[#2086](https://github.com/rust-lang/mdBook/pull/2086)
|
||||
[#2082](https://github.com/rust-lang/mdBook/pull/2082)
|
||||
[#2084](https://github.com/rust-lang/mdBook/pull/2084)
|
||||
[#2085](https://github.com/rust-lang/mdBook/pull/2085)
|
||||
|
||||
### Fixed
|
||||
- Switched from the `gitignore` library to `ignore`. This should bring some improvements with gitignore handling.
|
||||
[#2076](https://github.com/rust-lang/mdBook/pull/2076)
|
||||
|
||||
## mdBook 0.4.28
|
||||
[v0.4.27...v0.4.28](https://github.com/rust-lang/mdBook/compare/v0.4.27...v0.4.28)
|
||||
|
||||
### Changed
|
||||
- The sidebar is now shown on wide screens when localstorage is disabled.
|
||||
[#2017](https://github.com/rust-lang/mdBook/pull/2017)
|
||||
- Preprocessors are now run with `mdbook test`.
|
||||
[#1986](https://github.com/rust-lang/mdBook/pull/1986)
|
||||
|
||||
### Fixed
|
||||
- Fixed regression in 0.4.26 that prevented the title bar from scrolling properly on smaller screens.
|
||||
[#2039](https://github.com/rust-lang/mdBook/pull/2039)
|
||||
|
||||
## mdBook 0.4.27
|
||||
[v0.4.26...v0.4.27](https://github.com/rust-lang/mdBook/compare/v0.4.26...v0.4.27)
|
||||
|
||||
### Changed
|
||||
- Reverted the dependency update to the `toml` crate. This was an unintentional breaking change in 0.4.26.
|
||||
[#2021](https://github.com/rust-lang/mdBook/pull/2021)
|
||||
|
||||
## mdBook 0.4.26
|
||||
[v0.4.25...v0.4.26](https://github.com/rust-lang/mdBook/compare/v0.4.25...v0.4.26)
|
||||
|
||||
**The 0.4.26 release has been yanked due to an unintentional breaking change.**
|
||||
|
||||
### Changed
|
||||
- Removed custom scrollbars for webkit browsers
|
||||
[#1961](https://github.com/rust-lang/mdBook/pull/1961)
|
||||
- Updated some dependencies
|
||||
[#1998](https://github.com/rust-lang/mdBook/pull/1998)
|
||||
[#2009](https://github.com/rust-lang/mdBook/pull/2009)
|
||||
[#2011](https://github.com/rust-lang/mdBook/pull/2011)
|
||||
- Fonts are now part of the theme.
|
||||
The `output.html.copy-fonts` option has been deprecated.
|
||||
To define custom fonts, be sure to define `theme/fonts.css`.
|
||||
[#1987](https://github.com/rust-lang/mdBook/pull/1987)
|
||||
|
||||
### Fixed
|
||||
- Fixed overflow viewport issue with mobile Safari
|
||||
[#1994](https://github.com/rust-lang/mdBook/pull/1994)
|
||||
|
||||
## mdBook 0.4.25
|
||||
[e14d381...1ba74a3](https://github.com/rust-lang/mdBook/compare/e14d381...1ba74a3)
|
||||
|
||||
### Fixed
|
||||
- Fixed a regression where `mdbook test -L deps path-to-book` would not work.
|
||||
[#1959](https://github.com/rust-lang/mdBook/pull/1959)
|
||||
|
||||
## mdBook 0.4.24
|
||||
[eb77083...8767ebf](https://github.com/rust-lang/mdBook/compare/eb77083...8767ebf)
|
||||
|
||||
### Fixed
|
||||
- The precompiled linux-gnu mdbook binary available on [GitHub Releases](https://github.com/rust-lang/mdBook/releases) inadvertently switched to a newer version of glibc. This release goes back to an older version that should be more compatible on older versions of Linux.
|
||||
[#1955](https://github.com/rust-lang/mdBook/pull/1955)
|
||||
|
||||
## mdBook 0.4.23
|
||||
[678b469...68a75da](https://github.com/rust-lang/mdBook/compare/678b469...68a75da)
|
||||
|
||||
### Changed
|
||||
- Updated all dependencies
|
||||
[#1951](https://github.com/rust-lang/mdBook/pull/1951)
|
||||
[#1952](https://github.com/rust-lang/mdBook/pull/1952)
|
||||
[#1844](https://github.com/rust-lang/mdBook/pull/1844)
|
||||
- Updated minimum Rust version to 1.60.
|
||||
[#1951](https://github.com/rust-lang/mdBook/pull/1951)
|
||||
|
||||
### Fixed
|
||||
- Fixed a regression where playground code was missing hidden lines, preventing it from compiling correctly.
|
||||
[#1950](https://github.com/rust-lang/mdBook/pull/1950)
|
||||
|
||||
## mdBook 0.4.22
|
||||
[40c06f5...4844f72](https://github.com/rust-lang/mdBook/compare/40c06f5...4844f72)
|
||||
|
||||
### Added
|
||||
- Added a `--chapter` option to `mdbook test` to specify a specific chapter to test.
|
||||
[#1741](https://github.com/rust-lang/mdBook/pull/1741)
|
||||
- Added CSS styling for `<kbd>` tags.
|
||||
[#1906](https://github.com/rust-lang/mdBook/pull/1906)
|
||||
- Added pre-compiled binaries for `x86_64-unknown-linux-musl` and `aarch64-unknown-linux-musl` (see [Releases](https://github.com/rust-lang/mdBook/releases)).
|
||||
[#1862](https://github.com/rust-lang/mdBook/pull/1862)
|
||||
- Added `build.extra-watch-dirs` which is an array of additional directories to watch for changes when running `mdbook serve`.
|
||||
[#1884](https://github.com/rust-lang/mdBook/pull/1884)
|
||||
|
||||
### Changed
|
||||
- Removed the `type="text/javascript"` attribute from `<script>` tags.
|
||||
[#1881](https://github.com/rust-lang/mdBook/pull/1881)
|
||||
- Switched to building with Rust Edition 2021.
|
||||
This raises the minimum supported Rust version to 1.56.
|
||||
[#1887](https://github.com/rust-lang/mdBook/pull/1887)
|
||||
- When hidden code is hidden, the hidden parts are no longer copied to the clipboard via the copy button.
|
||||
[#1911](https://github.com/rust-lang/mdBook/pull/1911)
|
||||
- Various HTML changes and fixes to be more compliant with HTML5.
|
||||
[#1924](https://github.com/rust-lang/mdBook/pull/1924)
|
||||
- The theme picker now shows which theme is currently selected.
|
||||
[#1935](https://github.com/rust-lang/mdBook/pull/1935)
|
||||
|
||||
### Fixed
|
||||
- Avoid blank line at the end of an ACE code block
|
||||
[#1836](https://github.com/rust-lang/mdBook/pull/1836)
|
||||
|
||||
|
||||
## mdBook 0.4.21
|
||||
[92afe9b...8f01d02](https://github.com/rust-lang/mdBook/compare/92afe9b...8f01d02)
|
||||
|
||||
### Fixed
|
||||
- Fixed an issue where mdBook would fail to compile with Rust nightly-2022-07-22.
|
||||
[#1861](https://github.com/rust-lang/mdBook/pull/1861)
|
||||
|
||||
## mdBook 0.4.20
|
||||
[53055e0...da166e0](https://github.com/rust-lang/mdBook/compare/53055e0...da166e0)
|
||||
|
||||
@@ -68,7 +405,7 @@
|
||||
[#1771](https://github.com/rust-lang/mdBook/pull/1771)
|
||||
- The 404 not-found page now includes the books title in the HTML title tag.
|
||||
[#1693](https://github.com/rust-lang/mdBook/pull/1693)
|
||||
- Migrated to clap 3.0 which which handles CLI option parsing.
|
||||
- Migrated to clap 3.0 which handles CLI option parsing.
|
||||
[#1731](https://github.com/rust-lang/mdBook/pull/1731)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -7,13 +7,22 @@ If you have come here to learn how to contribute to mdBook, we have some tips fo
|
||||
First of all, don't hesitate to ask questions!
|
||||
Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple.
|
||||
|
||||
### Issue assignment
|
||||
|
||||
**:warning: Important :warning:**
|
||||
|
||||
Before working on pull request, please ping us on the corresponding issue.
|
||||
The current PR backlog is beyond what we can process at this time.
|
||||
Only issues that have an [`E-Help-wanted`](https://github.com/rust-lang/mdBook/labels/E-Help-wanted) or [`Feature accepted`](https://github.com/rust-lang/mdBook/labels/Feature%20accepted) label will likely receive reviews.
|
||||
If there isn't already an open issue for what you want to work on, please open one first to see if it is something we would be available to review.
|
||||
|
||||
### Issues to work on
|
||||
|
||||
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
|
||||
If you are starting out, you might be interested in the
|
||||
[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.
|
||||
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/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
@@ -21,16 +30,16 @@ If you come from a web development background, you might be interested in issues
|
||||
[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.
|
||||
When you decide you want to work on a specific issue, and it isn't already assigned to someone else, assign the issue to yourself by leaving a comment with the text `@rustbot claim`.
|
||||
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
|
||||
|
||||
Issues on the issue tracker are categorized with the following labels:
|
||||
|
||||
- **A**-prefixed labels state which area of the project an issue relates to.
|
||||
- **E**-prefixed labels show an estimate of the experience necessary to fix the issue.
|
||||
- **M**-prefixed labels are meta-issues used for questions, discussions, or tracking issues
|
||||
- **M**-prefixed labels are meta-issues regarding the management of the mdBook project itself
|
||||
- **S**-prefixed labels show the status of the issue
|
||||
- **T**-prefixed labels show the type of issue
|
||||
- **C**-prefixed labels show the category of issue
|
||||
|
||||
### Building mdBook
|
||||
|
||||
@@ -59,7 +68,7 @@ This will ensure we have good quality source code that is better for us all to m
|
||||
[rustfmt](https://github.com/rust-lang/rustfmt) has a lot more information on the project.
|
||||
The quick guide is
|
||||
|
||||
1. Install it
|
||||
1. Install it (`rustfmt` is usually installed by default via [rustup](https://rustup.rs/)):
|
||||
```
|
||||
rustup component add rustfmt
|
||||
```
|
||||
@@ -71,18 +80,15 @@ The quick guide is
|
||||
```
|
||||
cargo fmt
|
||||
```
|
||||
When run through `cargo` it will format all bin and lib files in the current crate.
|
||||
When run through `cargo` it will format all bin and lib files in the current package.
|
||||
|
||||
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
|
||||
|
||||
Clippy is a code analyser/linter detecting mistakes, and therefore helps to improve your code.
|
||||
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
|
||||
help us maintain awesome code.
|
||||
|
||||
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang/rust-clippy)
|
||||
[Clippy](https://doc.rust-lang.org/clippy/) is a code analyser/linter detecting mistakes, and therefore helps to improve your code.
|
||||
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will help us maintain awesome code.
|
||||
|
||||
1. To install
|
||||
```
|
||||
@@ -93,17 +99,36 @@ The best documentation can be found over at [rust-clippy](https://github.com/rus
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang.github.io/rust-clippy/master/index.html).
|
||||
### Change requirements
|
||||
|
||||
Please consider the following when making a change:
|
||||
|
||||
* Almost all changes that modify the Rust code must be accompanied with a test.
|
||||
|
||||
* Almost all features and changes must update the documentation.
|
||||
mdBook has the [mdBook Guide](https://rust-lang.github.io/mdBook/) whose source is at <https://github.com/rust-lang/mdBook/tree/master/guide>.
|
||||
|
||||
* Almost all Rust items should be documented with doc comments.
|
||||
See the [Rustdoc Book](https://doc.rust-lang.org/rustdoc/) for more information on writing doc comments.
|
||||
|
||||
* Breaking the API can only be done in major SemVer releases.
|
||||
These are done very infrequently, so it is preferred to avoid these when possible.
|
||||
See [SemVer Compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for more information on what a SemVer breaking change is.
|
||||
|
||||
(Note: At this time, some SemVer breaking changes are inevitable due to the current code structure.
|
||||
An example is adding new fields to the config structures.
|
||||
These are intended to be fixed in the next major release.)
|
||||
|
||||
* Similarly, the CLI interface is considered to be stable.
|
||||
Care should be taken to avoid breaking existing workflows.
|
||||
|
||||
* Check out the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) for guidelines on designing the API.
|
||||
|
||||
### Making a pull-request
|
||||
|
||||
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||
One of the core maintainers will then approve the changes or request some changes before it gets merged.
|
||||
|
||||
If you want to make your pull-request even better, you might want to run [Clippy](https://github.com/Manishearth/rust-clippy)
|
||||
and [rustfmt](https://github.com/rust-lang/rustfmt) on the code first.
|
||||
This is not a requirement though and will never block a pull-request from being merged.
|
||||
|
||||
That's it, happy contributions! :tada: :tada: :tada:
|
||||
|
||||
## Browser compatibility and testing
|
||||
@@ -116,15 +141,35 @@ If possible, do your best to avoid breaking older browser releases.
|
||||
Any change to the HTML or styling is encouraged to manually check on as many browsers and platforms that you can.
|
||||
Unfortunately at this time we don't have any automated UI or browser testing, so your assistance in testing is appreciated.
|
||||
|
||||
## Updating higlight.js
|
||||
## Updating highlight.js
|
||||
|
||||
The following are instructions for updating [highlight.js](https://highlightjs.org/).
|
||||
|
||||
1. Clone the repository at <https://github.com/highlightjs/highlight.js>
|
||||
1. Check out a tagged release (like `10.1.1`).
|
||||
1. Run `npm install`
|
||||
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx properties r scala x86asm yaml`
|
||||
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx nim nix properties r scala x86asm yaml`
|
||||
1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`.
|
||||
1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js).
|
||||
1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release.
|
||||
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [test_book](https://github.com/rust-lang/mdBook/tree/master/test_book) contains a chapter with many languages to examine.
|
||||
|
||||
## Publishing new releases
|
||||
|
||||
Instructions for mdBook maintainers to publish a new release:
|
||||
|
||||
1. Create a PR to update the version and update the CHANGELOG:
|
||||
1. Update the version in `Cargo.toml`
|
||||
2. Run `cargo test` to verify that everything is passing, and to update `Cargo.lock`.
|
||||
3. Double-check for any SemVer breaking changes.
|
||||
Try [`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks), though beware that the current version of mdBook isn't properly adhering to SemVer due to the lack of `#[non_exhaustive]` and other issues. See https://github.com/rust-lang/mdBook/issues/1835.
|
||||
4. Update `CHANGELOG.md` with any changes that users may be interested in.
|
||||
5. Update `continuous-integration.md` to update the version number for the installation instructions.
|
||||
6. Commit the changes, and open a PR.
|
||||
2. After the PR has been merged, create a release in GitHub. This can either be done in the GitHub web UI, or on the command-line:
|
||||
```bash
|
||||
MDBOOK_VERS="`cargo read-manifest | jq -r .version`" ; \
|
||||
gh release create -R rust-lang/mdbook v$MDBOOK_VERS \
|
||||
--title v$MDBOOK_VERS \
|
||||
--notes "See https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md#mdbook-${MDBOOK_VERS//.} for a complete list of changes."
|
||||
```
|
||||
|
||||
2280
Cargo.lock
generated
2280
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
91
Cargo.toml
91
Cargo.toml
@@ -1,67 +1,84 @@
|
||||
[workspace]
|
||||
members = [".", "examples/remove-emphasis/mdbook-remove-emphasis"]
|
||||
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.20"
|
||||
version = "0.4.41"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
"Matt Ickstadt <mattico8@gmail.com>"
|
||||
]
|
||||
documentation = "http://rust-lang.github.io/mdBook/index.html"
|
||||
edition = "2018"
|
||||
documentation = "https://rust-lang.github.io/mdBook/index.html"
|
||||
edition = "2021"
|
||||
exclude = ["/guide/*"]
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
description = "Creates a book from markdown files"
|
||||
rust-version = "1.74"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.28"
|
||||
chrono = "0.4"
|
||||
clap = { version = "3.0", features = ["cargo"] }
|
||||
clap_complete = "3.0"
|
||||
env_logger = "0.9.0"
|
||||
handlebars = "4.0"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
memchr = "2.0"
|
||||
opener = "0.5"
|
||||
pulldown-cmark = { version = "0.9.1", default-features = false }
|
||||
regex = "1.5.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shlex = "1"
|
||||
tempfile = "3.0"
|
||||
toml = "0.5.1"
|
||||
topological-sort = "0.1.0"
|
||||
anyhow = "1.0.71"
|
||||
chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.3.12", features = ["cargo", "wrap_help"] }
|
||||
clap_complete = "4.3.2"
|
||||
once_cell = "1.17.1"
|
||||
env_logger = "0.11.1"
|
||||
handlebars = "6.0"
|
||||
log = "0.4.17"
|
||||
memchr = "2.5.0"
|
||||
opener = "0.7.0"
|
||||
pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] } # Do not update, part of the public api.
|
||||
regex = "1.8.1"
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
shlex = "1.3.0"
|
||||
tempfile = "3.4.0"
|
||||
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
|
||||
topological-sort = "0.2.2"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "4.0", optional = true }
|
||||
gitignore = { version = "1.0", optional = true }
|
||||
notify = { version = "6.1.1", optional = true }
|
||||
notify-debouncer-mini = { version = "0.4.1", optional = true }
|
||||
ignore = { version = "0.4.20", optional = true }
|
||||
pathdiff = { version = "0.2.1", optional = true }
|
||||
walkdir = { version = "2.3.3", optional = true }
|
||||
|
||||
# Serve feature
|
||||
futures-util = { version = "0.3.4", optional = true }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.2", default-features = false, features = ["websocket"], optional = true }
|
||||
futures-util = { version = "0.3.28", optional = true }
|
||||
tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.6", default-features = false, features = ["websocket"], optional = true }
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "3.0.0", optional = true }
|
||||
ammonia = { version = "3", optional = true }
|
||||
elasticlunr-rs = { version = "3.0.2", optional = true }
|
||||
ammonia = { version = "4.0.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "1"
|
||||
predicates = "2"
|
||||
select = "0.5"
|
||||
semver = "1.0"
|
||||
pretty_assertions = "1.2.1"
|
||||
walkdir = "2.0"
|
||||
assert_cmd = "2.0.11"
|
||||
predicates = "3.0.3"
|
||||
select = "0.6.0"
|
||||
semver = "1.0.17"
|
||||
pretty_assertions = "1.3.0"
|
||||
walkdir = "2.3.3"
|
||||
|
||||
[features]
|
||||
default = ["watch", "serve", "search"]
|
||||
watch = ["notify", "gitignore"]
|
||||
serve = ["futures-util", "tokio", "warp"]
|
||||
search = ["elasticlunr-rs", "ammonia"]
|
||||
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"]
|
||||
serve = ["dep:futures-util", "dep:tokio", "dep:warp"]
|
||||
search = ["dep:elasticlunr-rs", "dep:ammonia"]
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
name = "mdbook"
|
||||
|
||||
[[example]]
|
||||
name = "nop-preprocessor"
|
||||
test = true
|
||||
|
||||
[[example]]
|
||||
name = "remove-emphasis"
|
||||
path = "examples/remove-emphasis/test.rs"
|
||||
crate-type = ["lib"]
|
||||
test = true
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/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 "$PWD/hub/bin" >> $GITHUB_PATH
|
||||
@@ -13,6 +13,30 @@ TOOLCHAIN="$1"
|
||||
rustup set profile minimal
|
||||
rustup component remove --toolchain=$TOOLCHAIN rust-docs || echo "already removed"
|
||||
rustup update --no-self-update $TOOLCHAIN
|
||||
if [ -n "$2" ]
|
||||
then
|
||||
TARGET="$2"
|
||||
HOST=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
|
||||
if [ "$HOST" != "$TARGET" ]
|
||||
then
|
||||
rustup component add llvm-tools-preview --toolchain=$TOOLCHAIN
|
||||
rustup component add rust-std-$TARGET --toolchain=$TOOLCHAIN
|
||||
fi
|
||||
if [[ $TARGET == *"musl" ]]
|
||||
then
|
||||
# This is needed by libdbus-sys.
|
||||
sudo apt update -y && sudo apt install musl-dev musl-tools -y
|
||||
fi
|
||||
if [[ $TARGET == "aarch64-unknown-linux-musl" ]]
|
||||
then
|
||||
echo CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=rust-lld >> $GITHUB_ENV
|
||||
# This `CC` is some nonsense needed for libdbus-sys (via opener).
|
||||
# I don't know if this is really the right thing to do, but it seems to work.
|
||||
sudo apt install gcc-aarch64-linux-gnu -y
|
||||
echo CC=aarch64-linux-gnu-gcc >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
|
||||
rustup default $TOOLCHAIN
|
||||
rustup -V
|
||||
rustc -Vv
|
||||
|
||||
@@ -11,16 +11,17 @@ fi
|
||||
TAG=${GITHUB_REF#*/tags/}
|
||||
|
||||
host=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
|
||||
target=$2
|
||||
export CARGO_PROFILE_RELEASE_LTO=true
|
||||
cargo build --bin mdbook --release
|
||||
cd target/release
|
||||
cargo build --locked --bin mdbook --release --target $target
|
||||
cd target/$target/release
|
||||
case $1 in
|
||||
ubuntu*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
asset="mdbook-$TAG-$target.tar.gz"
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
macos*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
asset="mdbook-$TAG-$target.tar.gz"
|
||||
# There is a bug with BSD tar on macOS where the first 8MB of the file are
|
||||
# sometimes all NUL bytes. See https://github.com/actions/cache/issues/403
|
||||
# and https://github.com/rust-lang/cargo/issues/8603 for some more
|
||||
@@ -30,7 +31,7 @@ case $1 in
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
windows*)
|
||||
asset="mdbook-$TAG-$host.zip"
|
||||
asset="mdbook-$TAG-$target.zip"
|
||||
7z a ../../$asset mdbook.exe
|
||||
;;
|
||||
*)
|
||||
@@ -39,9 +40,10 @@ case $1 in
|
||||
esac
|
||||
cd ../..
|
||||
|
||||
if [[ -z "$GITHUB_TOKEN" ]]
|
||||
if [[ -z "$GITHUB_ENV" ]]
|
||||
then
|
||||
echo "$GITHUB_TOKEN not set, skipping deploy."
|
||||
echo "GITHUB_ENV not set, run: gh release upload $TAG target/$asset"
|
||||
else
|
||||
hub release edit -m "" --attach $asset $TAG
|
||||
echo "MDBOOK_TAG=$TAG" >> $GITHUB_ENV
|
||||
echo "MDBOOK_ASSET=target/$asset" >> $GITHUB_ENV
|
||||
fi
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::nop_lib::Nop;
|
||||
use clap::{App, Arg, ArgMatches};
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use mdbook::book::Book;
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||
@@ -7,11 +7,11 @@ use semver::{Version, VersionReq};
|
||||
use std::io;
|
||||
use std::process;
|
||||
|
||||
pub fn make_app() -> App<'static> {
|
||||
App::new("nop-preprocessor")
|
||||
pub fn make_app() -> Command {
|
||||
Command::new("nop-preprocessor")
|
||||
.about("A mdbook preprocessor which does precisely nothing")
|
||||
.subcommand(
|
||||
App::new("supports")
|
||||
Command::new("supports")
|
||||
.arg(Arg::new("renderer").required(true))
|
||||
.about("Check whether a renderer is supported by this preprocessor"),
|
||||
)
|
||||
@@ -26,7 +26,7 @@ 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);
|
||||
eprintln!("{e}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,9 @@ fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
||||
}
|
||||
|
||||
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
|
||||
let renderer = sub_args.value_of("renderer").expect("Required argument");
|
||||
let renderer = sub_args
|
||||
.get_one::<String>("renderer")
|
||||
.expect("Required argument");
|
||||
let supported = pre.supports_renderer(renderer);
|
||||
|
||||
// Signal whether the renderer is supported by exiting with 1 or 0.
|
||||
@@ -101,4 +103,58 @@ mod nop_lib {
|
||||
renderer != "not-supported"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn nop_preprocessor_run() {
|
||||
let input_json = r##"[
|
||||
{
|
||||
"root": "/path/to/book",
|
||||
"config": {
|
||||
"book": {
|
||||
"authors": ["AUTHOR"],
|
||||
"language": "en",
|
||||
"multilingual": false,
|
||||
"src": "src",
|
||||
"title": "TITLE"
|
||||
},
|
||||
"preprocessor": {
|
||||
"nop": {}
|
||||
}
|
||||
},
|
||||
"renderer": "html",
|
||||
"mdbook_version": "0.4.21"
|
||||
},
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"Chapter": {
|
||||
"name": "Chapter 1",
|
||||
"content": "# Chapter 1\n",
|
||||
"number": [1],
|
||||
"sub_items": [],
|
||||
"path": "chapter_1.md",
|
||||
"source_path": "chapter_1.md",
|
||||
"parent_names": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"__non_exhaustive": null
|
||||
}
|
||||
]"##;
|
||||
let input_json = input_json.as_bytes();
|
||||
|
||||
let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
|
||||
let expected_book = book.clone();
|
||||
let result = Nop::new().run(&ctx, book);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// The nop-preprocessor should not have made any changes to the book content.
|
||||
let actual_book = result.unwrap();
|
||||
assert_eq!(actual_book, expected_book);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
examples/remove-emphasis/.gitignore
vendored
Normal file
1
examples/remove-emphasis/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
book
|
||||
5
examples/remove-emphasis/book.toml
Normal file
5
examples/remove-emphasis/book.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[book]
|
||||
title = "remove-emphasis"
|
||||
|
||||
[preprocessor.remove-emphasis]
|
||||
command = "cargo run --manifest-path=mdbook-remove-emphasis/Cargo.toml --locked"
|
||||
10
examples/remove-emphasis/mdbook-remove-emphasis/Cargo.toml
Normal file
10
examples/remove-emphasis/mdbook-remove-emphasis/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "mdbook-remove-emphasis"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
mdbook = { version = "0.4.40", path = "../../.." }
|
||||
pulldown-cmark = { version = "0.12.2", default-features = false }
|
||||
pulldown-cmark-to-cmark = "18.0.0"
|
||||
serde_json = "1.0.132"
|
||||
82
examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs
Normal file
82
examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
//! This is a demonstration of an mdBook preprocessor which parses markdown
|
||||
//! and removes any instances of emphasis.
|
||||
|
||||
use mdbook::book::{Book, Chapter};
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use mdbook::BookItem;
|
||||
use pulldown_cmark::{Event, Parser, Tag, TagEnd};
|
||||
use std::io;
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
match args.next().as_deref() {
|
||||
Some("supports") => {
|
||||
// Supports all renderers.
|
||||
return;
|
||||
}
|
||||
Some(arg) => {
|
||||
eprintln!("unknown argument: {arg}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
if let Err(e) = handle_preprocessing() {
|
||||
eprintln!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoveEmphasis;
|
||||
|
||||
impl Preprocessor for RemoveEmphasis {
|
||||
fn name(&self) -> &str {
|
||||
"remove-emphasis"
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
|
||||
let mut total = 0;
|
||||
book.for_each_mut(|item| {
|
||||
let BookItem::Chapter(ch) = item else {
|
||||
return;
|
||||
};
|
||||
if ch.is_draft_chapter() {
|
||||
return;
|
||||
}
|
||||
match remove_emphasis(&mut total, ch) {
|
||||
Ok(s) => ch.content = s,
|
||||
Err(e) => eprintln!("failed to process chapter: {e:?}"),
|
||||
}
|
||||
});
|
||||
eprintln!("removed {total} emphasis");
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
// ANCHOR: remove_emphasis
|
||||
fn remove_emphasis(num_removed_items: &mut usize, chapter: &mut Chapter) -> Result<String, Error> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
let events = Parser::new(&chapter.content).filter(|e| match e {
|
||||
Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong) => {
|
||||
*num_removed_items += 1;
|
||||
false
|
||||
}
|
||||
Event::End(TagEnd::Emphasis) | Event::End(TagEnd::Strong) => false,
|
||||
_ => true,
|
||||
});
|
||||
|
||||
Ok(pulldown_cmark_to_cmark::cmark(events, &mut buf).map(|_| buf)?)
|
||||
}
|
||||
// ANCHOR_END: remove_emphasis
|
||||
|
||||
pub fn handle_preprocessing() -> Result<(), Error> {
|
||||
let pre = RemoveEmphasis;
|
||||
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
||||
|
||||
let processed_book = pre.run(&ctx, book)?;
|
||||
serde_json::to_writer(io::stdout(), &processed_book)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
3
examples/remove-emphasis/src/SUMMARY.md
Normal file
3
examples/remove-emphasis/src/SUMMARY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Summary
|
||||
|
||||
- [Chapter 1](./chapter_1.md)
|
||||
3
examples/remove-emphasis/src/chapter_1.md
Normal file
3
examples/remove-emphasis/src/chapter_1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Chapter 1
|
||||
|
||||
This has *light emphasis* and **bold emphasis**.
|
||||
13
examples/remove-emphasis/test.rs
Normal file
13
examples/remove-emphasis/test.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use mdbook::MDBook;
|
||||
|
||||
#[test]
|
||||
fn remove_emphasis_works() {
|
||||
// Tests that the remove-emphasis example works as expected.
|
||||
|
||||
// Workaround for https://github.com/rust-lang/mdBook/issues/1424
|
||||
std::env::set_current_dir("examples/remove-emphasis").unwrap();
|
||||
let book = MDBook::load(".").unwrap();
|
||||
book.build().unwrap();
|
||||
let ch1 = std::fs::read_to_string("book/chapter_1.html").unwrap();
|
||||
assert!(ch1.contains("This has light emphasis and bold emphasis."));
|
||||
}
|
||||
@@ -8,6 +8,7 @@ language = "en"
|
||||
edition = "2018"
|
||||
|
||||
[output.html]
|
||||
smart-punctuation = true
|
||||
mathjax-support = true
|
||||
site-url = "/mdBook/"
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
|
||||
@@ -17,6 +18,9 @@ edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path
|
||||
editable = true
|
||||
line-numbers = true
|
||||
|
||||
[output.html.code.hidelines]
|
||||
python = "~"
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 20
|
||||
use-boolean-and = true
|
||||
|
||||
@@ -5,10 +5,10 @@ After you have [installed](../guide/installation.md) `mdbook`, you can run the `
|
||||
|
||||
This following sections provide in-depth information on the different commands available.
|
||||
|
||||
* [`mdbook init <directory>`](init.md) — Creates a new book with minimal boilerplate to start with.
|
||||
* [`mdbook build`](build.md) — Renders the book.
|
||||
* [`mdbook watch`](watch.md) — Rebuilds the book any time a source file changes.
|
||||
* [`mdbook serve`](serve.md) — Runs a web server to view the book, and rebuilds on changes.
|
||||
* [`mdbook test`](test.md) — Tests Rust code samples.
|
||||
* [`mdbook clean`](clean.md) — Deletes the rendered output.
|
||||
* [`mdbook completions`](completions.md) — Support for shell auto-completion.
|
||||
* [`mdbook init <directory>`](init.md) --- Creates a new book with minimal boilerplate to start with.
|
||||
* [`mdbook build`](build.md) --- Renders the book.
|
||||
* [`mdbook watch`](watch.md) --- Rebuilds the book any time a source file changes.
|
||||
* [`mdbook serve`](serve.md) --- Runs a web server to view the book, and rebuilds on changes.
|
||||
* [`mdbook test`](test.md) --- Tests Rust code samples.
|
||||
* [`mdbook clean`](clean.md) --- Deletes the rendered output.
|
||||
* [`mdbook completions`](completions.md) --- Support for shell auto-completion.
|
||||
|
||||
7
guide/src/cli/arg-watcher.md
Normal file
7
guide/src/cli/arg-watcher.md
Normal file
@@ -0,0 +1,7 @@
|
||||
#### `--watcher`
|
||||
|
||||
There are different backends used to determine when a file has changed.
|
||||
|
||||
* `poll` (default) --- Checks for file modifications by scanning the filesystem every second.
|
||||
* `native` --- Uses the native operating system facilities to receive notifications when files change.
|
||||
This can have less constant overhead, but may not be as reliable as the `poll` based watcher. See these issues for more information: [#383](https://github.com/rust-lang/mdBook/issues/383) [#1441](https://github.com/rust-lang/mdBook/issues/1441) [#1707](https://github.com/rust-lang/mdBook/issues/1707) [#2035](https://github.com/rust-lang/mdBook/issues/2035) [#2102](https://github.com/rust-lang/mdBook/issues/2102)
|
||||
@@ -7,8 +7,8 @@ mdbook build
|
||||
```
|
||||
|
||||
It will try to parse your `SUMMARY.md` file to understand the structure of your
|
||||
book and fetch the corresponding files. Note that files mentioned in `SUMMARY.md`
|
||||
but not present will be created.
|
||||
book and fetch the corresponding files. Note that this will also create files
|
||||
mentioned in `SUMMARY.md` which are not yet present.
|
||||
|
||||
The rendered output will maintain the same directory structure as the source for
|
||||
convenience. Large books will therefore remain structured when rendered.
|
||||
@@ -22,12 +22,12 @@ root instead of the current working directory.
|
||||
mdbook build path/to/book
|
||||
```
|
||||
|
||||
#### --open
|
||||
#### `--open`
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the rendered book in
|
||||
your default web browser after building it.
|
||||
|
||||
#### --dest-dir
|
||||
#### `--dest-dir`
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
|
||||
@@ -16,7 +16,7 @@ root instead of the current working directory.
|
||||
mdbook clean path/to/book
|
||||
```
|
||||
|
||||
#### --dest-dir
|
||||
#### `--dest-dir`
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to override the book's output
|
||||
directory, which will be deleted by this command. Relative paths are interpreted
|
||||
@@ -27,4 +27,4 @@ value of the `build.build-dir` key in `book.toml`, or to `./book`.
|
||||
mdbook clean --dest-dir=path/to/book
|
||||
```
|
||||
|
||||
`path/to/book` could be absolute or relative.
|
||||
`path/to/book` could be absolute or relative.
|
||||
|
||||
@@ -6,7 +6,11 @@ This means when you type `mdbook` in your shell, you can then press your shell's
|
||||
The completions first need to be installed for your shell:
|
||||
|
||||
```bash
|
||||
# bash
|
||||
mdbook completions bash > ~/.local/share/bash-completion/completions/mdbook
|
||||
# oh-my-zsh
|
||||
mdbook completions zsh > ~/.oh-my-zsh/completions/_mdbook
|
||||
autoload -U compinit && compinit
|
||||
```
|
||||
|
||||
The command prints a completion script for the given shell.
|
||||
|
||||
@@ -45,7 +45,7 @@ instead of the current working directory.
|
||||
mdbook init path/to/book
|
||||
```
|
||||
|
||||
#### --theme
|
||||
#### `--theme`
|
||||
|
||||
When you use the `--theme` flag, the default theme will be copied into a
|
||||
directory called `theme` in your source directory so that you can modify it.
|
||||
@@ -53,7 +53,7 @@ directory called `theme` in your source directory so that you can modify it.
|
||||
The theme is selectively overwritten, this means that if you don't want to
|
||||
overwrite a specific file, just delete it and the default file will be used.
|
||||
|
||||
#### --title
|
||||
#### `--title`
|
||||
|
||||
Specify a title for the book. If not supplied, an interactive prompt will ask for
|
||||
a title.
|
||||
@@ -62,9 +62,21 @@ a title.
|
||||
mdbook init --title="my amazing book"
|
||||
```
|
||||
|
||||
#### --ignore
|
||||
#### `--ignore`
|
||||
|
||||
Create a `.gitignore` file configured to ignore the `book` directory created when [building] a book.
|
||||
If not supplied, an interactive prompt will ask whether it should be created.
|
||||
|
||||
```bash
|
||||
mdbook init --ignore=none
|
||||
```
|
||||
|
||||
```bash
|
||||
mdbook init --ignore=git
|
||||
```
|
||||
|
||||
[building]: build.md
|
||||
|
||||
#### `--force`
|
||||
|
||||
Skip the prompts to create a `.gitignore` and for the title for the book.
|
||||
|
||||
@@ -32,18 +32,20 @@ The `serve` hostname defaults to `localhost`, and the port defaults to `3000`. E
|
||||
mdbook serve path/to/book -p 8000 -n 127.0.0.1
|
||||
```
|
||||
|
||||
#### --open
|
||||
#### `--open`
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in your
|
||||
default web browser after starting the server.
|
||||
|
||||
#### --dest-dir
|
||||
#### `--dest-dir`
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
{{#include arg-watcher.md}}
|
||||
|
||||
#### Specify exclude patterns
|
||||
|
||||
The `serve` command will not automatically trigger a build for files listed in
|
||||
|
||||
@@ -6,8 +6,7 @@ of code examples that could get outdated. Therefore it is very important for
|
||||
them to be able to automatically test these code examples.
|
||||
|
||||
mdBook supports a `test` command that will run all available tests in a book. At
|
||||
the moment, only rustdoc tests are supported, but this may be expanded upon in
|
||||
the future.
|
||||
the moment, only Rust tests are supported.
|
||||
|
||||
#### Disable tests on a code block
|
||||
|
||||
@@ -38,12 +37,12 @@ instead of the current working directory.
|
||||
mdbook test path/to/book
|
||||
```
|
||||
|
||||
#### --library-path
|
||||
#### `--library-path`
|
||||
|
||||
The `--library-path` (`-L`) option allows you to add directories to the library
|
||||
search path used by `rustdoc` when it builds and tests the examples. Multiple
|
||||
directories can be specified with multiple options (`-L foo -L bar`) or with a
|
||||
comma-delimited list (`-L foo,bar`). The path should point to the Cargo
|
||||
comma-delimited list (`-L foo,bar`). The path should point to the Cargo
|
||||
[build cache](https://doc.rust-lang.org/cargo/guide/build-cache.html) `deps` directory that
|
||||
contains the build output of your project. For example, if your Rust project's book is in a directory
|
||||
named `my-book`, the following command would include the crate's dependencies when running `test`:
|
||||
@@ -55,9 +54,14 @@ mdbook test my-book -L target/debug/deps/
|
||||
See the `rustdoc` command-line [documentation](https://doc.rust-lang.org/rustdoc/command-line-arguments.html#-l--library-path-where-to-look-for-dependencies)
|
||||
for more information.
|
||||
|
||||
#### --dest-dir
|
||||
#### `--dest-dir`
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
#### `--chapter`
|
||||
|
||||
The `--chapter` (`-c`) option allows you to test a specific chapter of the
|
||||
book using the chapter name or the relative path to the chapter.
|
||||
|
||||
@@ -15,18 +15,19 @@ root instead of the current working directory.
|
||||
mdbook watch path/to/book
|
||||
```
|
||||
|
||||
#### --open
|
||||
#### `--open`
|
||||
|
||||
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
|
||||
your default web browser.
|
||||
|
||||
#### --dest-dir
|
||||
#### `--dest-dir`
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
{{#include arg-watcher.md}}
|
||||
|
||||
#### Specify exclude patterns
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex
|
||||
|
||||
```sh
|
||||
mkdir bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.20/mdbook-v0.4.20-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.41/mdbook-v0.4.41-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
bin/mdbook build
|
||||
```
|
||||
|
||||
@@ -51,13 +51,13 @@ cargo install mdbook --no-default-features --features search --vers "^0.4" --loc
|
||||
|
||||
This includes several recommended options:
|
||||
|
||||
* `--no-default-features` — Disables features like the HTTP server used by `mdbook serve` that is likely not needed on CI.
|
||||
* `--no-default-features` --- Disables features like the HTTP server used by `mdbook serve` that is likely not needed on CI.
|
||||
This will speed up the build time significantly.
|
||||
* `--features search` — Disabling default features means you should then manually enable features that you want, such as the built-in [search] capability.
|
||||
* `--vers "^0.4"` — This will install the most recent version of the `0.4` series.
|
||||
* `--features search` --- Disabling default features means you should then manually enable features that you want, such as the built-in [search] capability.
|
||||
* `--vers "^0.4"` --- This will install the most recent version of the `0.4` series.
|
||||
However, versions after like `0.5.0` won't be installed, as they may break your build.
|
||||
Cargo will automatically upgrade mdBook if you have an older version already installed.
|
||||
* `--locked` — This will use the dependencies that were used when mdBook was released.
|
||||
* `--locked` --- This will use the dependencies that were used when mdBook was released.
|
||||
Without `--locked`, it will use the latest version of all dependencies, which may include some fixes since the last release, but may also (rarely) cause build problems.
|
||||
|
||||
You will likely want to investigate caching options, as building mdBook can be somewhat slow.
|
||||
@@ -83,7 +83,7 @@ Or if you have your own style checks, spell checker, or any other tests it might
|
||||
## Deploying
|
||||
|
||||
You may want to automatically deploy your book.
|
||||
Some may want to do this with every time a change is pushed, and others may want to only deploy when a specific release is tagged.
|
||||
Some may want to do this every time a change is pushed, and others may want to only deploy when a specific release is tagged.
|
||||
|
||||
You'll also need to understand the specifics on how to push a change to your web service.
|
||||
For example, [GitHub Pages] just requires committing the output onto a specific git branch.
|
||||
|
||||
@@ -287,7 +287,7 @@ like this:
|
||||
+ if cfg.deny_odds && num_words % 2 == 1 {
|
||||
+ eprintln!("{} has an odd number of words!", ch.name);
|
||||
+ process::exit(1);
|
||||
}
|
||||
+ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,33 +68,10 @@ The following code block shows how to remove all emphasis from markdown,
|
||||
without accidentally breaking the document.
|
||||
|
||||
```rust
|
||||
fn remove_emphasis(
|
||||
num_removed_items: &mut usize,
|
||||
chapter: &mut Chapter,
|
||||
) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
|
||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
||||
Error::from(format!("Markdown serialization failed: {}", err))
|
||||
})
|
||||
}
|
||||
{{#rustdoc_include ../../../examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs:remove_emphasis}}
|
||||
```
|
||||
|
||||
For everything else, have a look [at the complete example][example].
|
||||
Take a look at the [full example source][emphasis-example] for more details.
|
||||
|
||||
## Implementing a preprocessor with a different language
|
||||
|
||||
@@ -122,11 +99,10 @@ if __name__ == '__main__':
|
||||
```
|
||||
|
||||
|
||||
|
||||
[emphasis-example]: https://github.com/rust-lang/mdBook/tree/master/examples/remove-emphasis/
|
||||
[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/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
|
||||
|
||||
@@ -29,7 +29,7 @@ book's title without needing to touch your `book.toml`.
|
||||
> building the book with something like
|
||||
>
|
||||
> ```shell
|
||||
> $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
|
||||
> $ export MDBOOK_BOOK='{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}'
|
||||
> $ mdbook build
|
||||
> ```
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@ This is general information about your book.
|
||||
`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.
|
||||
This is also used to derive the direction of text (RTL, LTR) within the book.
|
||||
- **text-direction**: The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL). Possible values: `ltr`, `rtl`.
|
||||
When not specified, the text direction is derived from the book's `language` attribute.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
@@ -55,6 +58,7 @@ 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"
|
||||
text-direction = "ltr"
|
||||
```
|
||||
|
||||
### Rust options
|
||||
@@ -68,7 +72,7 @@ edition = "2015" # the default edition for code blocks
|
||||
```
|
||||
|
||||
- **edition**: Rust edition to use by default for the code snippets. Default
|
||||
is "2015". Individual code blocks can be controlled with the `edition2015`,
|
||||
is `"2015"`. Individual code blocks can be controlled with the `edition2015`,
|
||||
`edition2018` or `edition2021` annotations, such as:
|
||||
|
||||
~~~text
|
||||
@@ -87,6 +91,7 @@ This controls the build process of your book.
|
||||
build-dir = "book" # the directory where the output is placed
|
||||
create-missing = true # whether or not to create missing pages
|
||||
use-default-preprocessors = true # use the default preprocessors
|
||||
extra-watch-dirs = [] # directories to watch for triggering builds
|
||||
```
|
||||
|
||||
- **build-dir:** The directory to put the rendered book in. By default this is
|
||||
@@ -96,7 +101,7 @@ use-default-preprocessors = true # use the default preprocessors
|
||||
will be created when the book is built (i.e. `create-missing = true`). If this
|
||||
is `false` then the build process will instead exit with an error if any files
|
||||
do not exist.
|
||||
- **use-default-preprocessors:** Disable the default preprocessors of (`links` &
|
||||
- **use-default-preprocessors:** Disable the default preprocessors (of `links` &
|
||||
`index`) by setting this option to `false`.
|
||||
|
||||
If you have the same, and/or other preprocessors declared via their table
|
||||
@@ -108,3 +113,6 @@ use-default-preprocessors = true # use the default preprocessors
|
||||
default preprocessors from running.
|
||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||
`use-default-preprocessors` that `links` it will run.
|
||||
- **extra-watch-dirs**: A list of paths to directories that will be watched in
|
||||
the `watch` and `serve` commands. Changes to files under these directories will
|
||||
trigger rebuilds. Useful if your book depends on files outside its `src` directory.
|
||||
|
||||
@@ -35,7 +35,7 @@ For example, if you have a preprocessor called `mdbook-example`, then you can in
|
||||
With this table, mdBook will execute the `mdbook-example` preprocessor.
|
||||
|
||||
This table can include additional key-value pairs that are specific to the preprocessor.
|
||||
For example, if our example prepocessor needed some extra configuration options:
|
||||
For example, if our example preprocessor needed some extra configuration options:
|
||||
|
||||
```toml
|
||||
[preprocessor.example]
|
||||
|
||||
@@ -4,9 +4,9 @@ Renderers (also called "backends") are responsible for creating the output of th
|
||||
|
||||
The following backends are built-in:
|
||||
|
||||
* [`html`](#html-renderer-options) — This renders the book to HTML.
|
||||
* [`html`](#html-renderer-options) --- This renders the book to HTML.
|
||||
This is enabled by default if no other `[output]` tables are defined in `book.toml`.
|
||||
* [`markdown`](#markdown-renderer) — This outputs the book as markdown after running the preprocessors.
|
||||
* [`markdown`](#markdown-renderer) --- This outputs the book as markdown after running the preprocessors.
|
||||
This is useful for debugging preprocessors.
|
||||
|
||||
The community has developed several backends.
|
||||
@@ -97,7 +97,7 @@ description = "The example book covers examples."
|
||||
theme = "my-theme"
|
||||
default-theme = "light"
|
||||
preferred-dark-theme = "navy"
|
||||
curly-quotes = true
|
||||
smart-punctuation = true
|
||||
mathjax-support = false
|
||||
copy-fonts = true
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
@@ -120,13 +120,18 @@ The following configuration options are available:
|
||||
'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)
|
||||
[`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`.
|
||||
- **smart-punctuation:** Converts quotes to curly quotes, `...` to `…`, `--` to en-dash, and `---` to em-dash.
|
||||
See [Smart Punctuation](../markdown.md#smart-punctuation).
|
||||
Defaults to `false`.
|
||||
- **curly-quotes:** Deprecated alias for `smart-punctuation`.
|
||||
- **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`.
|
||||
- **copy-fonts:** (**Deprecated**) If `true` (the default), mdBook uses its built-in fonts which are copied to the output directory.
|
||||
If `false`, the built-in fonts will not be used.
|
||||
This option is deprecated. If you want to define your own custom fonts,
|
||||
create a `theme/fonts/fonts.css` file and store the fonts in the `theme/fonts/` directory.
|
||||
- **google-analytics:** This field has been deprecated and will be removed in a future release.
|
||||
Use the `theme/head.hbs` file to add the appropriate Google Analytics code instead.
|
||||
- **additional-css:** If you need to slightly change the appearance of your book
|
||||
@@ -147,9 +152,9 @@ The following configuration options are available:
|
||||
- **edit-url-template:** Edit url template, when provided shows a
|
||||
"Suggest an edit" button (which looks like <i class="fa fa-edit"></i>) for directly jumping to editing the currently
|
||||
viewed page. For e.g. GitHub projects set this to
|
||||
`https://github.com/<owner>/<repo>/edit/master/{path}` or for
|
||||
`https://github.com/<owner>/<repo>/edit/<branch>/{path}` or for
|
||||
Bitbucket projects set it to
|
||||
`https://bitbucket.org/<owner>/<repo>/src/master/{path}?mode=edit`
|
||||
`https://bitbucket.org/<owner>/<repo>/src/<branch>/{path}?mode=edit`
|
||||
where {path} will be replaced with the full path of the file in the
|
||||
repository.
|
||||
- **input-404:** The name of the markdown file used for missing files.
|
||||
@@ -157,7 +162,8 @@ The following configuration options are available:
|
||||
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 `/`.
|
||||
urls in subdirectories. Defaults to `/`. If `site-url` is set,
|
||||
make sure to use document relative links for your assets, meaning they should not start with `/`.
|
||||
- **cname:** The DNS subdomain or apex domain at which your book will be hosted.
|
||||
This string will be written to a file named CNAME in the root of your site, as
|
||||
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages
|
||||
@@ -178,7 +184,7 @@ page-break = true # insert page-break after each chapter
|
||||
|
||||
- **enable:** Enable print support. When `false`, all print support will not be
|
||||
rendered. Defaults to `true`.
|
||||
- **page-break** Insert page breaks between chapters. Defaults to `true`.
|
||||
- **page-break:** Insert page breaks between chapters. Defaults to `true`.
|
||||
|
||||
### `[output.html.fold]`
|
||||
|
||||
@@ -214,11 +220,25 @@ runnable = true # displays a run button for rust code
|
||||
- **copyable:** Display the copy button on code snippets. Defaults to `true`.
|
||||
- **copy-js:** Copy JavaScript files for the editor to the output directory.
|
||||
Defaults to `true`.
|
||||
- **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`.
|
||||
- **runnable** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. Defaults to `true`.
|
||||
- **line-numbers:** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`.
|
||||
- **runnable:** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. Defaults to `true`.
|
||||
|
||||
[Ace]: https://ace.c9.io/
|
||||
|
||||
### `[output.html.code]`
|
||||
|
||||
The `[output.html.code]` table provides options for controlling code blocks.
|
||||
|
||||
```toml
|
||||
[output.html.code]
|
||||
# A prefix string per language (one or more chars).
|
||||
# Any line starting with whitespace+prefix is hidden.
|
||||
hidelines = { python = "~" }
|
||||
```
|
||||
|
||||
- **hidelines:** A table that defines how [hidden code lines](../mdbook.md#hiding-code-lines) work for each language.
|
||||
The key is the language and the value is a string that will cause code lines starting with that prefix to be hidden.
|
||||
|
||||
### `[output.html.search]`
|
||||
|
||||
The `[output.html.search]` table provides options for controlling the built-in text [search].
|
||||
|
||||
@@ -73,14 +73,14 @@ Linking to a URL or local file is easy:
|
||||
```markdown
|
||||
Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
Read about [mdBook](mdBook.md).
|
||||
Read about [mdBook](mdbook.md).
|
||||
|
||||
A bare url: <https://www.rust-lang.org>.
|
||||
```
|
||||
|
||||
Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
Read about [mdBook](mdBook.md).
|
||||
Read about [mdBook](mdbook.md).
|
||||
|
||||
A bare url: <https://www.rust-lang.org>.
|
||||
|
||||
@@ -124,7 +124,7 @@ mdBook has several extensions beyond the standard CommonMark specification.
|
||||
### Strikethrough
|
||||
|
||||
Text may be rendered with a horizontal line through the center by wrapping the
|
||||
text with two tilde characters on each side:
|
||||
text with one or two tilde characters on each side:
|
||||
|
||||
```text
|
||||
An example of ~~strikethrough text~~.
|
||||
@@ -214,9 +214,22 @@ characters:
|
||||
So, no need to manually enter those Unicode characters!
|
||||
|
||||
This feature is disabled by default.
|
||||
To enable it, see the [`output.html.curly-quotes`] config option.
|
||||
To enable it, see the [`output.html.smart-punctuation`] config option.
|
||||
|
||||
[strikethrough]: https://github.github.com/gfm/#strikethrough-extension-
|
||||
[tables]: https://github.github.com/gfm/#tables-extension-
|
||||
[task list extension]: https://github.github.com/gfm/#task-list-items-extension-
|
||||
[`output.html.curly-quotes`]: configuration/renderers.md#html-renderer-options
|
||||
[`output.html.smart-punctuation`]: configuration/renderers.md#html-renderer-options
|
||||
|
||||
### Heading attributes
|
||||
|
||||
Headings can have a custom HTML ID and classes. This lets you maintain the same ID even if you change the heading's text, it also lets you add multiple classes in the heading.
|
||||
|
||||
Example:
|
||||
```md
|
||||
# Example heading { #first .class1 .class2 }
|
||||
```
|
||||
|
||||
This makes the level 1 heading with the content `Example heading`, ID `first`, and classes `class1` and `class2`. Note that the attributes should be space-separated.
|
||||
|
||||
More information can be found in the [heading attrs spec page](https://github.com/raphlinus/pulldown-cmark/blob/master/pulldown-cmark/specs/heading_attrs.txt).
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
||||
with a `#` [like you would with Rustdoc][rustdoc-hide].
|
||||
This currently only works with Rust language code blocks.
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them with a specific prefix.
|
||||
|
||||
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example
|
||||
For the Rust language, you can use the `#` character as a prefix which will hide lines [like you would with Rustdoc][rustdoc-hide].
|
||||
|
||||
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/write-documentation/documentation-tests.html#hiding-portions-of-the-example
|
||||
|
||||
```bash
|
||||
# fn main() {
|
||||
@@ -28,7 +28,47 @@ Will render as
|
||||
# }
|
||||
```
|
||||
|
||||
The code block has an eyeball icon (<i class="fa fa-eye"></i>) which will toggle the visibility of the hidden lines.
|
||||
When you tap or hover the mouse over the code block, there will be an eyeball icon (<i class="fa fa-eye"></i>) which will toggle the visibility of the hidden lines.
|
||||
|
||||
By default, this only works for code examples that are annotated with `rust`.
|
||||
However, you can define custom prefixes for other languages by adding a new line-hiding prefix in your `book.toml` with the language name and prefix character(s):
|
||||
|
||||
```toml
|
||||
[output.html.code.hidelines]
|
||||
python = "~"
|
||||
```
|
||||
|
||||
The prefix will hide any lines that begin with the given prefix. With the python prefix shown above, this:
|
||||
|
||||
```bash
|
||||
~hidden()
|
||||
nothidden():
|
||||
~ hidden()
|
||||
~hidden()
|
||||
nothidden()
|
||||
```
|
||||
|
||||
will render as
|
||||
|
||||
```python
|
||||
~hidden()
|
||||
nothidden():
|
||||
~ hidden()
|
||||
~hidden()
|
||||
nothidden()
|
||||
```
|
||||
|
||||
This behavior can be overridden locally with a different prefix. This has the same effect as above:
|
||||
|
||||
~~~markdown
|
||||
```python,hidelines=!!!
|
||||
!!!hidden()
|
||||
nothidden():
|
||||
!!! hidden()
|
||||
!!!hidden()
|
||||
nothidden()
|
||||
```
|
||||
~~~
|
||||
|
||||
## Rust Playground
|
||||
|
||||
@@ -72,16 +112,16 @@ panic!("oops!");
|
||||
These are particularly important when using [`mdbook test`] to test Rust examples.
|
||||
These use the same attributes as [rustdoc attributes], with a few additions:
|
||||
|
||||
* `editable` — Enables the [editor].
|
||||
* `noplayground` — Removes the play button, but will still be tested.
|
||||
* `mdbook-runnable` — Forces the play button to be displayed.
|
||||
* `editable` --- Enables the [editor].
|
||||
* `noplayground` --- Removes the play button, but will still be tested.
|
||||
* `mdbook-runnable` --- Forces the play button to be displayed.
|
||||
This is intended to be combined with the `ignore` attribute for examples that should not be tested, but you want to allow the reader to run.
|
||||
* `ignore` — Will not be tested and no play button is shown, but it is still highlighted as Rust syntax.
|
||||
* `should_panic` — When executed, it should produce a panic.
|
||||
* `no_run` — The code is compiled when tested, but it is not run.
|
||||
* `ignore` --- Will not be tested and no play button is shown, but it is still highlighted as Rust syntax.
|
||||
* `should_panic` --- When executed, it should produce a panic.
|
||||
* `no_run` --- The code is compiled when tested, but it is not run.
|
||||
The play button is also not shown.
|
||||
* `compile_fail` — The code should fail to compile.
|
||||
* `edition2015`, `edition2018`, `edition2021` — Forces the use of a specific Rust edition.
|
||||
* `compile_fail` --- The code should fail to compile.
|
||||
* `edition2015`, `edition2018`, `edition2021` --- Forces the use of a specific Rust edition.
|
||||
See [`rust.edition`] to set this globally.
|
||||
|
||||
[`mdbook test`]: ../cli/test.md
|
||||
@@ -274,3 +314,51 @@ contents (sidebar) by including a `\{{#title ...}}` near the top of the page.
|
||||
```hbs
|
||||
\{{#title My Title}}
|
||||
```
|
||||
|
||||
## HTML classes provided by mdBook
|
||||
|
||||
<img class="right" src="images/rust-logo-blk.svg" alt="The Rust logo">
|
||||
|
||||
### `class="left"` and `"right"`
|
||||
|
||||
These classes are provided by default, for inline HTML to float images.
|
||||
|
||||
```html
|
||||
<img class="right" src="images/rust-logo-blk.svg" alt="The Rust logo">
|
||||
```
|
||||
|
||||
### `class="hidden"`
|
||||
|
||||
HTML tags with class `hidden` will not be shown.
|
||||
|
||||
```html
|
||||
<div class="hidden">This will not be seen.</div>
|
||||
```
|
||||
|
||||
<div class="hidden">This will not be seen.</div>
|
||||
|
||||
### `class="warning"`
|
||||
|
||||
To make a warning or similar note stand out, wrap it in a warning div.
|
||||
|
||||
```html
|
||||
<div class="warning">
|
||||
|
||||
This is a bad thing that you should pay attention to.
|
||||
|
||||
Warning blocks should be used sparingly in documentation, to avoid "warning
|
||||
fatigue," where people are trained to ignore them because they usually don't
|
||||
matter for what they're doing.
|
||||
|
||||
</div>
|
||||
```
|
||||
|
||||
<div class="warning">
|
||||
|
||||
This is a bad thing that you should pay attention to.
|
||||
|
||||
Warning blocks should be used sparingly in documentation, to avoid "warning
|
||||
fatigue," where people are trained to ignore them because they usually don't
|
||||
matter for what they're doing.
|
||||
|
||||
</div>
|
||||
|
||||
@@ -29,11 +29,12 @@ to be ignored at best, or may cause an error when attempting to build the book.
|
||||
- [First Chapter](relative/path/to/markdown2.md)
|
||||
```
|
||||
|
||||
1. ***Part Title*** - Headers can be used as a title for the following numbered
|
||||
chapters. This can be used to logically separate different sections
|
||||
of the book. The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many
|
||||
parts as desired.
|
||||
1. ***Part Title*** -
|
||||
Level 1 headers can be used as a title for the following numbered chapters.
|
||||
This can be used to logically separate different sections of the book.
|
||||
The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many parts as desired.
|
||||
Part titles must be h1 headers (one `#`), other heading levels are ignored.
|
||||
```markdown
|
||||
# My Part Title
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Theme
|
||||
|
||||
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to
|
||||
The default renderer uses a [handlebars](https://handlebarsjs.com) template to
|
||||
render your markdown files and comes with a default theme included in the mdBook
|
||||
binary.
|
||||
|
||||
@@ -26,6 +26,8 @@ Here are the files you can override:
|
||||
- **_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].
|
||||
- **fonts/fonts.css** contains the definition of which fonts to load.
|
||||
Custom fonts can be included in the `fonts` directory.
|
||||
|
||||
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
|
||||
|
||||
@@ -18,7 +18,7 @@ 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`, as specified in `book.toml` (if not specified, defaults to `en`). To use in <code
|
||||
class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||
class="language-html">\<html lang=\"{{ language }}\"></code> for example.
|
||||
- ***title*** Title used for the current page. This is identical to `{{ chapter_title }} - {{ book_title }}` unless `book_title` is not set in which case it just defaults to the `chapter_title`.
|
||||
- ***book_title*** Title of the book, as specified in `book.toml`
|
||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||
@@ -79,7 +79,7 @@ var chapters = {{chapters}};
|
||||
|
||||
### 2. previous / next
|
||||
|
||||
The previous and next helpers expose a `link` and `name` property to the
|
||||
The previous and next helpers expose a `link` and `title` property to the
|
||||
previous and next chapters.
|
||||
|
||||
They are used like this
|
||||
@@ -87,7 +87,7 @@ They are used like this
|
||||
```handlebars
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
<i class="fa fa-angle-left"></i> {{title}}
|
||||
</a>
|
||||
{{/previous}}
|
||||
```
|
||||
|
||||
@@ -44,6 +44,8 @@ your own `highlight.js` file:
|
||||
- makefile
|
||||
- markdown
|
||||
- nginx
|
||||
- nim
|
||||
- nix
|
||||
- objectivec
|
||||
- perl
|
||||
- php
|
||||
@@ -77,38 +79,6 @@ the `theme` folder of your book.
|
||||
|
||||
Now your theme will be used instead of the default theme.
|
||||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
||||
with a `#`.
|
||||
|
||||
|
||||
```bash
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 6;
|
||||
|
||||
println!("{}", x + y);
|
||||
# }
|
||||
```
|
||||
|
||||
Will render as
|
||||
|
||||
```rust
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 7;
|
||||
|
||||
println!("{}", x + y);
|
||||
# }
|
||||
```
|
||||
|
||||
**At the moment, this only works for code examples that are annotated with
|
||||
`rust`. Because it would collide with semantics of some programming languages.
|
||||
In the future, we want to make this configurable through the `book.toml` so that
|
||||
everyone can benefit from it.**
|
||||
|
||||
|
||||
## Improve default theme
|
||||
|
||||
If you think the default theme doesn't look quite right for a specific language,
|
||||
|
||||
@@ -20,7 +20,7 @@ To make it easier to run, put the path to the binary into your `PATH`.
|
||||
|
||||
To build the `mdbook` executable from source, you will first need to install Rust and Cargo.
|
||||
Follow the instructions on the [Rust installation page].
|
||||
mdBook currently requires at least Rust version 1.54.
|
||||
mdBook currently requires at least Rust version 1.74.
|
||||
|
||||
Once you have installed Rust, the following command can be used to build and install mdBook:
|
||||
|
||||
@@ -30,6 +30,9 @@ cargo install mdbook
|
||||
|
||||
This will automatically download mdBook from [crates.io], build it, and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||
|
||||
You can run `cargo install mdbook` again whenever you want to update to a new version.
|
||||
That command will check if there is a newer version, and re-install mdBook if a newer version is found.
|
||||
|
||||
To uninstall, run the command `cargo uninstall mdbook`.
|
||||
|
||||
[Rust installation page]: https://www.rust-lang.org/tools/install
|
||||
@@ -47,6 +50,8 @@ cargo install --git https://github.com/rust-lang/mdBook.git mdbook
|
||||
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
## Modifying and contributing
|
||||
|
||||
If you are interested in making modifications to mdBook itself, check out the [Contributing Guide] for more information.
|
||||
|
||||
[Contributing Guide]: https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
use crate::config::BuildConfig;
|
||||
use crate::errors::*;
|
||||
use crate::utils::bracket_escape;
|
||||
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Load a book into memory from its `src/` directory.
|
||||
@@ -18,11 +18,11 @@ pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>
|
||||
|
||||
let mut summary_content = String::new();
|
||||
File::open(&summary_md)
|
||||
.with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))?
|
||||
.with_context(|| format!("Couldn't open SUMMARY.md in {src_dir:?} directory"))?
|
||||
.read_to_string(&mut summary_content)?;
|
||||
|
||||
let summary = parse_summary(&summary_content)
|
||||
.with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
|
||||
.with_context(|| format!("Summary parsing failed for file={summary_md:?}"))?;
|
||||
|
||||
if cfg.create_missing {
|
||||
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
|
||||
@@ -39,9 +39,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||
.chain(summary.suffix_chapters.iter())
|
||||
.collect();
|
||||
|
||||
while !items.is_empty() {
|
||||
let next = items.pop().expect("already checked");
|
||||
|
||||
while let Some(next) = items.pop() {
|
||||
if let SummaryItem::Link(ref link) = *next {
|
||||
if let Some(ref location) = link.location {
|
||||
let filename = src_dir.join(location);
|
||||
@@ -162,8 +160,20 @@ pub struct Chapter {
|
||||
/// Nested items.
|
||||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
///
|
||||
/// **Note**: After the index preprocessor runs, any README files will be
|
||||
/// modified to be `index.md`. If you need access to the actual filename
|
||||
/// on disk, use [`Chapter::source_path`] instead.
|
||||
///
|
||||
/// This is `None` for a draft chapter.
|
||||
pub path: Option<PathBuf>,
|
||||
/// The chapter's source file, relative to the `SUMMARY.md` file.
|
||||
///
|
||||
/// **Note**: Beware that README files will internally be treated as
|
||||
/// `index.md` via the [`Chapter::path`] field. The `source_path` field
|
||||
/// exists if you need access to the true file path.
|
||||
///
|
||||
/// This is `None` for a draft chapter.
|
||||
pub source_path: Option<PathBuf>,
|
||||
/// An ordered list of the names of each chapter above this one in the hierarchy.
|
||||
pub parent_names: Vec<String>,
|
||||
@@ -277,7 +287,7 @@ fn load_chapter<P: AsRef<Path>>(
|
||||
}
|
||||
|
||||
let stripped = location
|
||||
.strip_prefix(&src_dir)
|
||||
.strip_prefix(src_dir)
|
||||
.expect("Chapters are always inside a book");
|
||||
|
||||
Chapter::new(&link.name, content, stripped, parent_names.clone())
|
||||
@@ -317,7 +327,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let item = self.items.pop_front();
|
||||
|
||||
if let Some(&BookItem::Chapter(ref ch)) = item {
|
||||
if let Some(BookItem::Chapter(ch)) = item {
|
||||
// if we wanted a breadth-first iterator we'd `extend()` here
|
||||
for sub_item in ch.sub_items.iter().rev() {
|
||||
self.items.push_front(sub_item);
|
||||
@@ -331,7 +341,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
impl Display for Chapter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ref section_number) = self.number {
|
||||
write!(f, "{} ", section_number)?;
|
||||
write!(f, "{section_number} ")?;
|
||||
}
|
||||
|
||||
write!(f, "{}", self.name)
|
||||
@@ -341,7 +351,6 @@ impl Display for Chapter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &str = "
|
||||
|
||||
@@ -6,6 +6,8 @@ use super::MDBook;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use crate::theme;
|
||||
use crate::utils::fs::write_file;
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
/// A helper for setting up a new book and its directory structure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -157,6 +159,19 @@ impl BookBuilder {
|
||||
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
|
||||
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
|
||||
|
||||
write_file(&themedir.join("fonts"), "fonts.css", theme::fonts::CSS)?;
|
||||
for (file_name, contents) in theme::fonts::LICENSES {
|
||||
write_file(&themedir, file_name, contents)?;
|
||||
}
|
||||
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
|
||||
write_file(&themedir, file_name, contents)?;
|
||||
}
|
||||
write_file(
|
||||
&themedir,
|
||||
theme::fonts::SOURCE_CODE_PRO.0,
|
||||
theme::fonts::SOURCE_CODE_PRO.1,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -183,8 +198,7 @@ impl BookBuilder {
|
||||
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")?;
|
||||
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.");
|
||||
@@ -197,10 +211,10 @@ impl BookBuilder {
|
||||
fs::create_dir_all(&self.root)?;
|
||||
|
||||
let src = self.root.join(&self.config.book.src);
|
||||
fs::create_dir_all(&src)?;
|
||||
fs::create_dir_all(src)?;
|
||||
|
||||
let build = self.root.join(&self.config.build.build_dir);
|
||||
fs::create_dir_all(&build)?;
|
||||
fs::create_dir_all(build)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
166
src/book/mod.rs
166
src/book/mod.rs
@@ -14,10 +14,11 @@ pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
|
||||
pub use self::init::BookBuilder;
|
||||
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use log::{debug, error, info, log_enabled, trace, warn};
|
||||
use std::ffi::OsString;
|
||||
use std::io::{IsTerminal, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::string::ToString;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
use topological_sort::TopologicalSort;
|
||||
@@ -70,22 +71,28 @@ impl MDBook {
|
||||
|
||||
config.update_from_env();
|
||||
|
||||
if config
|
||||
.html_config()
|
||||
.map_or(false, |html| html.google_analytics.is_some())
|
||||
{
|
||||
warn!(
|
||||
"The output.html.google-analytics field has been deprecated; \
|
||||
it will be removed in a future release.\n\
|
||||
Consider placing the appropriate site tag code into the \
|
||||
theme/head.hbs file instead.\n\
|
||||
The tracking code may be found in the Google Analytics Admin page.\n\
|
||||
"
|
||||
);
|
||||
if let Some(html_config) = config.html_config() {
|
||||
if html_config.google_analytics.is_some() {
|
||||
warn!(
|
||||
"The output.html.google-analytics field has been deprecated; \
|
||||
it will be removed in a future release.\n\
|
||||
Consider placing the appropriate site tag code into the \
|
||||
theme/head.hbs file instead.\n\
|
||||
The tracking code may be found in the Google Analytics Admin page.\n\
|
||||
"
|
||||
);
|
||||
}
|
||||
if html_config.curly_quotes {
|
||||
warn!(
|
||||
"The output.html.curly-quotes field has been renamed to \
|
||||
output.html.smart-punctuation.\n\
|
||||
Use the new name in book.toml to remove this warning."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if log_enabled!(log::Level::Trace) {
|
||||
for line in format!("Config: {:#?}", config).lines() {
|
||||
for line in format!("Config: {config:#?}").lines() {
|
||||
trace!("{}", line);
|
||||
}
|
||||
}
|
||||
@@ -98,7 +105,7 @@ impl MDBook {
|
||||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = book::load_book(&src_dir, &config.build)?;
|
||||
let book = book::load_book(src_dir, &config.build)?;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
let preprocessors = determine_preprocessors(&config)?;
|
||||
@@ -121,7 +128,7 @@ impl 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 book = book::load_book_from_disk(&summary, src_dir)?;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
let preprocessors = determine_preprocessors(&config)?;
|
||||
@@ -195,21 +202,26 @@ impl MDBook {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular [`Renderer`].
|
||||
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
/// Run preprocessors and return the final book.
|
||||
pub fn preprocess_book(&self, renderer: &dyn Renderer) -> Result<(Book, PreprocessorContext)> {
|
||||
let preprocess_ctx = PreprocessorContext::new(
|
||||
self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string(),
|
||||
);
|
||||
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
Ok((preprocessed_book, preprocess_ctx))
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular [`Renderer`].
|
||||
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||
let (preprocessed_book, preprocess_ctx) = self.preprocess_book(renderer)?;
|
||||
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
@@ -246,22 +258,52 @@ impl MDBook {
|
||||
|
||||
/// Run `rustdoc` tests on the book, linking against the provided libraries.
|
||||
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
||||
let library_args: Vec<&str> = (0..library_paths.len())
|
||||
.map(|_| "-L")
|
||||
.zip(library_paths.into_iter())
|
||||
.flat_map(|x| vec![x.0, x.1])
|
||||
// test_chapter with chapter:None will run all tests.
|
||||
self.test_chapter(library_paths, None)
|
||||
}
|
||||
|
||||
/// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
|
||||
/// If `chapter` is `None`, all tests will be run.
|
||||
pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let library_args: Vec<OsString> = library_paths
|
||||
.into_iter()
|
||||
.flat_map(|path| {
|
||||
let path = Path::new(path);
|
||||
let path = if path.is_relative() {
|
||||
cwd.join(path).into_os_string()
|
||||
} else {
|
||||
path.to_path_buf().into_os_string()
|
||||
};
|
||||
[OsString::from("-L"), path]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
let preprocess_context =
|
||||
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
|
||||
let mut chapter_found = false;
|
||||
|
||||
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
|
||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
||||
// actual markdown files.
|
||||
struct TestRenderer;
|
||||
impl Renderer for TestRenderer {
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
fn name(&self) -> &str {
|
||||
"test"
|
||||
}
|
||||
|
||||
fn render(&self, _: &RenderContext) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Index Preprocessor is disabled so that chapter paths
|
||||
// continue to point to the actual markdown files.
|
||||
self.preprocessors = determine_preprocessors(&self.config)?
|
||||
.into_iter()
|
||||
.filter(|pre| pre.name() != IndexPreprocessor::NAME)
|
||||
.collect();
|
||||
let (book, _) = self.preprocess_book(&TestRenderer)?;
|
||||
|
||||
let color_output = std::io::stderr().is_terminal();
|
||||
let mut failed = false;
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
@@ -270,31 +312,50 @@ impl MDBook {
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let path = self.source_dir().join(&chapter_path);
|
||||
info!("Testing file: {:?}", path);
|
||||
if let Some(chapter) = chapter {
|
||||
if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
|
||||
if chapter == "?" {
|
||||
info!("Skipping chapter '{}'...", ch.name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
chapter_found = true;
|
||||
info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
|
||||
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(&chapter_path);
|
||||
let path = temp_dir.path().join(chapter_path);
|
||||
let mut tmpf = utils::fs::create_file(&path)?;
|
||||
tmpf.write_all(ch.content.as_bytes())?;
|
||||
|
||||
let mut cmd = Command::new("rustdoc");
|
||||
cmd.arg(&path).arg("--test").args(&library_args);
|
||||
cmd.current_dir(temp_dir.path())
|
||||
.arg(chapter_path)
|
||||
.arg("--test")
|
||||
.args(&library_args);
|
||||
|
||||
if let Some(edition) = self.config.rust.edition {
|
||||
match edition {
|
||||
RustEdition::E2015 => {
|
||||
cmd.args(&["--edition", "2015"]);
|
||||
cmd.args(["--edition", "2015"]);
|
||||
}
|
||||
RustEdition::E2018 => {
|
||||
cmd.args(&["--edition", "2018"]);
|
||||
cmd.args(["--edition", "2018"]);
|
||||
}
|
||||
RustEdition::E2021 => {
|
||||
cmd.args(&["--edition", "2021"]);
|
||||
cmd.args(["--edition", "2021"]);
|
||||
}
|
||||
RustEdition::E2024 => {
|
||||
cmd.args(["--edition", "2024", "-Zunstable-options"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if color_output {
|
||||
cmd.args(["--color", "always"]);
|
||||
}
|
||||
|
||||
debug!("running {:?}", cmd);
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
@@ -311,6 +372,11 @@ impl MDBook {
|
||||
if failed {
|
||||
bail!("One or more tests failed");
|
||||
}
|
||||
if let Some(chapter) = chapter {
|
||||
if !chapter_found {
|
||||
bail!("Chapter not found: {}", chapter);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -417,15 +483,13 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>
|
||||
if let Some(before) = table.get("before") {
|
||||
let before = before.as_array().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.before to be an array",
|
||||
name
|
||||
"Expected preprocessor.{name}.before to be an array"
|
||||
))
|
||||
})?;
|
||||
for after in before {
|
||||
let after = after.as_str().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.before to contain strings",
|
||||
name
|
||||
"Expected preprocessor.{name}.before to contain strings"
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -444,16 +508,12 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>
|
||||
|
||||
if let Some(after) = table.get("after") {
|
||||
let after = after.as_array().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.after to be an array",
|
||||
name
|
||||
))
|
||||
Error::msg(format!("Expected preprocessor.{name}.after to be an array"))
|
||||
})?;
|
||||
for before in after {
|
||||
let before = before.as_str().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.after to contain strings",
|
||||
name
|
||||
"Expected preprocessor.{name}.after to contain strings"
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -515,7 +575,7 @@ fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
|
||||
.get("command")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| format!("mdbook-{}", key))
|
||||
.unwrap_or_else(|| format!("mdbook-{key}"))
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
@@ -526,7 +586,7 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
.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))
|
||||
}
|
||||
@@ -564,7 +624,7 @@ fn preprocessor_should_run(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use toml::value::{Table, Value};
|
||||
use toml::value::Table;
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_html_renderer_if_empty() {
|
||||
@@ -720,7 +780,7 @@ mod tests {
|
||||
for preprocessor in &preprocessors {
|
||||
eprintln!(" {}", preprocessor.name());
|
||||
}
|
||||
panic!("{} should come before {}", before, after);
|
||||
panic!("{before} should come before {after}");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::errors::*;
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Event, HeadingLevel, Tag};
|
||||
use log::{debug, trace, warn};
|
||||
use memchr::Memchr;
|
||||
use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::iter::FromIterator;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -162,7 +162,7 @@ impl From<Link> for SummaryItem {
|
||||
/// > match the following regex: "[^<>\n[]]+".
|
||||
struct SummaryParser<'a> {
|
||||
src: &'a str,
|
||||
stream: pulldown_cmark::OffsetIter<'a, 'a>,
|
||||
stream: pulldown_cmark::OffsetIter<'a, DefaultBrokenLinkCallback>,
|
||||
offset: usize,
|
||||
|
||||
/// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
|
||||
@@ -209,7 +209,7 @@ macro_rules! collect_events {
|
||||
}
|
||||
|
||||
impl<'a> SummaryParser<'a> {
|
||||
fn new(text: &str) -> SummaryParser<'_> {
|
||||
fn new(text: &'a str) -> SummaryParser<'a> {
|
||||
let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
|
||||
|
||||
SummaryParser {
|
||||
@@ -264,7 +264,12 @@ impl<'a> SummaryParser<'a> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(ev @ Event::Start(Tag::List(..)))
|
||||
| Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
| Some(
|
||||
ev @ Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
}),
|
||||
) => {
|
||||
if is_prefix {
|
||||
// we've finished prefix chapters and are at the start
|
||||
// of the numbered section.
|
||||
@@ -274,8 +279,8 @@ impl<'a> SummaryParser<'a> {
|
||||
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
|
||||
}
|
||||
}
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let link = self.parse_link(href.to_string());
|
||||
Some(Event::Start(Tag::Link { dest_url, .. })) => {
|
||||
let link = self.parse_link(dest_url.to_string());
|
||||
items.push(SummaryItem::Link(link));
|
||||
}
|
||||
Some(Event::Rule) => items.push(SummaryItem::Separator),
|
||||
@@ -303,10 +308,13 @@ impl<'a> SummaryParser<'a> {
|
||||
break;
|
||||
}
|
||||
|
||||
Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
Some(Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
})) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
|
||||
let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
|
||||
Some(stringify_events(tags))
|
||||
}
|
||||
|
||||
@@ -335,7 +343,7 @@ impl<'a> SummaryParser<'a> {
|
||||
/// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened.
|
||||
fn parse_link(&mut self, href: String) -> Link {
|
||||
let href = href.replace("%20", " ");
|
||||
let link_content = collect_events!(self.stream, end Tag::Link(..));
|
||||
let link_content = collect_events!(self.stream, end TagEnd::Link);
|
||||
let name = stringify_events(link_content);
|
||||
|
||||
let path = if href.is_empty() {
|
||||
@@ -376,7 +384,12 @@ impl<'a> SummaryParser<'a> {
|
||||
}
|
||||
// The expectation is that pulldown cmark will terminate a paragraph before a new
|
||||
// heading, so we can always count on this to return without skipping headings.
|
||||
Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
Some(
|
||||
ev @ Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
}),
|
||||
) => {
|
||||
// we're starting a new part
|
||||
self.back(ev);
|
||||
break;
|
||||
@@ -397,7 +410,7 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
// Skip over the contents of this tag
|
||||
while let Some(event) = self.next_event() {
|
||||
if event == Event::End(other_tag.clone()) {
|
||||
if event == Event::End(other_tag.clone().into()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -453,7 +466,7 @@ impl<'a> SummaryParser<'a> {
|
||||
items.push(item);
|
||||
}
|
||||
Some(Event::Start(Tag::List(..))) => {
|
||||
// Skip this tag after comment bacause it is not nested.
|
||||
// Skip this tag after comment because it is not nested.
|
||||
if items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -468,7 +481,7 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
last_item.nested_items = sub_items;
|
||||
}
|
||||
Some(Event::End(Tag::List(..))) => break,
|
||||
Some(Event::End(TagEnd::List(..))) => break,
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
@@ -485,8 +498,8 @@ impl<'a> SummaryParser<'a> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Paragraph)) => continue,
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let mut link = self.parse_link(href.to_string());
|
||||
Some(Event::Start(Tag::Link { dest_url, .. })) => {
|
||||
let mut link = self.parse_link(dest_url.to_string());
|
||||
|
||||
let mut number = parent.clone();
|
||||
number.0.push(num_existing_items as u32 + 1);
|
||||
@@ -528,14 +541,18 @@ impl<'a> SummaryParser<'a> {
|
||||
fn parse_title(&mut self) -> Option<String> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
Some(Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
})) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
|
||||
let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
|
||||
return Some(stringify_events(tags));
|
||||
}
|
||||
// Skip a HTML element such as a comment line.
|
||||
Some(Event::Html(_)) => {}
|
||||
Some(Event::Html(_) | Event::InlineHtml(_))
|
||||
| Some(Event::Start(Tag::HtmlBlock) | Event::End(TagEnd::HtmlBlock)) => {}
|
||||
// Otherwise, no title.
|
||||
Some(ev) => {
|
||||
self.back(ev);
|
||||
@@ -566,11 +583,13 @@ fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
|
||||
.rev()
|
||||
.next()
|
||||
.ok_or_else(||
|
||||
anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links")
|
||||
.next_back()
|
||||
.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
|
||||
@@ -597,7 +616,7 @@ impl Display for SectionNumber {
|
||||
write!(f, "0")
|
||||
} else {
|
||||
for item in &self.0 {
|
||||
write!(f, "{}.", item)?;
|
||||
write!(f, "{item}.")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -743,8 +762,8 @@ mod tests {
|
||||
let _ = parser.stream.next(); // Discard opening paragraph
|
||||
|
||||
let href = match parser.stream.next() {
|
||||
Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(),
|
||||
other => panic!("Unreachable, {:?}", other),
|
||||
Some((Event::Start(Tag::Link { dest_url, .. }), _range)) => dest_url.to_string(),
|
||||
other => panic!("Unreachable, {other:?}"),
|
||||
};
|
||||
|
||||
let got = parser.parse_link(href);
|
||||
|
||||
@@ -1,42 +1,30 @@
|
||||
use super::command_prelude::*;
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("build")
|
||||
pub fn make_subcommand() -> Command {
|
||||
Command::new("build")
|
||||
.about("Builds a book from its markdown files")
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||
.arg_dest_dir()
|
||||
.arg_root_dir()
|
||||
.arg_open()
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
let mut book = MDBook::load(book_dir)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
||||
book.build()?;
|
||||
|
||||
if args.is_present("open") {
|
||||
if args.get_flag("open") {
|
||||
// FIXME: What's the right behaviour if we don't use the HTML renderer?
|
||||
let path = book.build_dir_for("html").join("index.html");
|
||||
if !path.exists() {
|
||||
|
||||
@@ -1,36 +1,24 @@
|
||||
use super::command_prelude::*;
|
||||
use crate::get_book_dir;
|
||||
use anyhow::Context;
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("clean")
|
||||
pub fn make_subcommand() -> Command {
|
||||
Command::new("clean")
|
||||
.about("Deletes a built book")
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg_dest_dir()
|
||||
.arg_root_dir()
|
||||
}
|
||||
|
||||
// Clean command implementation
|
||||
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
let book = MDBook::load(book_dir)?;
|
||||
|
||||
let dir_to_remove = match args.value_of("dest-dir") {
|
||||
let dir_to_remove = match args.get_one::<PathBuf>("dest-dir") {
|
||||
Some(dest_dir) => dest_dir.into(),
|
||||
None => book.root.join(&book.config.build.build_dir),
|
||||
};
|
||||
|
||||
59
src/cmd/command_prelude.rs
Normal file
59
src/cmd/command_prelude.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Helpers for building the command-line arguments for commands.
|
||||
|
||||
pub use clap::{arg, Arg, ArgMatches, Command};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub trait CommandExt: Sized {
|
||||
fn _arg(self, arg: Arg) -> Self;
|
||||
|
||||
fn arg_dest_dir(self) -> Self {
|
||||
self._arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.value_parser(clap::value_parser!(PathBuf))
|
||||
.help(
|
||||
"Output directory for the book\n\
|
||||
Relative paths are interpreted relative to the book's root directory.\n\
|
||||
If omitted, mdBook uses build.build-dir from book.toml \
|
||||
or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn arg_root_dir(self) -> Self {
|
||||
self._arg(
|
||||
Arg::new("dir")
|
||||
.help(
|
||||
"Root directory for the book\n\
|
||||
(Defaults to the current directory when omitted)",
|
||||
)
|
||||
.value_parser(clap::value_parser!(PathBuf)),
|
||||
)
|
||||
}
|
||||
|
||||
fn arg_open(self) -> Self {
|
||||
self._arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "watch", feature = "serve"))]
|
||||
fn arg_watcher(self) -> Self {
|
||||
#[cfg(feature = "watch")]
|
||||
return self._arg(
|
||||
Arg::new("watcher")
|
||||
.long("watcher")
|
||||
.value_parser(["poll", "native"])
|
||||
.default_value("poll")
|
||||
.help("The filesystem watching technique"),
|
||||
);
|
||||
#[cfg(not(feature = "watch"))]
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandExt for Command {
|
||||
fn _arg(self, arg: Arg) -> Self {
|
||||
self.arg(arg)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use clap::{arg, ArgMatches, Command as ClapCommand};
|
||||
use mdbook::config;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
@@ -8,30 +8,22 @@ use std::io::Write;
|
||||
use std::process::Command;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("init")
|
||||
pub fn make_subcommand() -> ClapCommand {
|
||||
ClapCommand::new("init")
|
||||
.about("Creates the boilerplate structure and files for a new book")
|
||||
// the {n} denotes a newline which will properly aligned in all help messages
|
||||
.arg(arg!([dir]
|
||||
"Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(
|
||||
arg!([dir]
|
||||
"Directory to create the book in\n\
|
||||
(Defaults to the current directory when omitted)"
|
||||
)
|
||||
.value_parser(clap::value_parser!(std::path::PathBuf)),
|
||||
)
|
||||
.arg(arg!(--theme "Copies the default theme into your source folder"))
|
||||
.arg(arg!(--force "Skips confirmation prompts"))
|
||||
.arg(arg!(--title <title> "Sets the book title"))
|
||||
.arg(
|
||||
Arg::new("title")
|
||||
.long("title")
|
||||
.takes_value(true)
|
||||
.help("Sets the book title")
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore")
|
||||
.long("ignore")
|
||||
.takes_value(true)
|
||||
.possible_values(&["none", "git"])
|
||||
.help("Creates a VCS ignore file (i.e. .gitignore)")
|
||||
.required(false),
|
||||
arg!(--ignore <ignore> "Creates a VCS ignore file (i.e. .gitignore)")
|
||||
.value_parser(["none", "git"]),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,12 +33,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let mut builder = MDBook::init(&book_dir);
|
||||
let mut config = config::Config::default();
|
||||
// If flag `--theme` is present, copy theme to src
|
||||
if args.is_present("theme") {
|
||||
if args.get_flag("theme") {
|
||||
let theme_dir = book_dir.join("theme");
|
||||
println!();
|
||||
println!("Copying the default theme to {}", theme_dir.display());
|
||||
// Skip this if `--force` is present
|
||||
if !args.is_present("force") && theme_dir.exists() {
|
||||
if !args.get_flag("force") && theme_dir.exists() {
|
||||
println!("This could potentially overwrite files already present in that directory.");
|
||||
print!("\nAre you sure you want to continue? (y/n) ");
|
||||
|
||||
@@ -59,20 +51,22 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ignore) = args.value_of("ignore") {
|
||||
if let Some(ignore) = args.get_one::<String>("ignore").map(|s| s.as_str()) {
|
||||
match ignore {
|
||||
"git" => builder.create_gitignore(true),
|
||||
_ => builder.create_gitignore(false),
|
||||
};
|
||||
} else {
|
||||
} else if !args.get_flag("force") {
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
if confirm() {
|
||||
builder.create_gitignore(true);
|
||||
}
|
||||
}
|
||||
|
||||
config.book.title = if args.is_present("title") {
|
||||
args.value_of("title").map(String::from)
|
||||
config.book.title = if args.contains_id("title") {
|
||||
args.get_one::<String>("title").map(String::from)
|
||||
} else if args.get_flag("force") {
|
||||
None
|
||||
} else {
|
||||
request_book_title()
|
||||
};
|
||||
@@ -92,7 +86,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
/// Obtains author name from git config file by running the `git config` command.
|
||||
fn get_author_name() -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.args(&["config", "--get", "user.name"])
|
||||
.args(["config", "--get", "user.name"])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
@@ -122,5 +116,5 @@ fn confirm() -> bool {
|
||||
io::stdout().flush().unwrap();
|
||||
let mut s = String::new();
|
||||
io::stdin().read_line(&mut s).ok();
|
||||
matches!(&*s.trim(), "Y" | "y" | "yes" | "Yes")
|
||||
matches!(s.trim(), "Y" | "y" | "yes" | "Yes")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod build;
|
||||
pub mod clean;
|
||||
pub mod command_prelude;
|
||||
pub mod init;
|
||||
#[cfg(feature = "serve")]
|
||||
pub mod serve;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use super::command_prelude::*;
|
||||
#[cfg(feature = "watch")]
|
||||
use super::watch;
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use clap::builder::NonEmptyStringValueParser;
|
||||
use futures_util::sink::SinkExt;
|
||||
use futures_util::StreamExt;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils;
|
||||
use mdbook::utils::fs::get_404_output_file;
|
||||
use mdbook::MDBook;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
@@ -18,43 +18,31 @@ use warp::Filter;
|
||||
const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("serve")
|
||||
pub fn make_subcommand() -> Command {
|
||||
Command::new("serve")
|
||||
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg_dest_dir()
|
||||
.arg_root_dir()
|
||||
.arg(
|
||||
Arg::new("hostname")
|
||||
.short('n')
|
||||
.long("hostname")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.default_value("localhost")
|
||||
.forbid_empty_values(true)
|
||||
.value_parser(NonEmptyStringValueParser::new())
|
||||
.help("Hostname to listen on for HTTP connections"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("port")
|
||||
.short('p')
|
||||
.long("port")
|
||||
.takes_value(true)
|
||||
.num_args(1)
|
||||
.default_value("3000")
|
||||
.forbid_empty_values(true)
|
||||
.value_parser(NonEmptyStringValueParser::new())
|
||||
.help("Port to use for HTTP connections"),
|
||||
)
|
||||
.arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||
.arg_open()
|
||||
.arg_watcher()
|
||||
}
|
||||
|
||||
// Serve command implementation
|
||||
@@ -62,17 +50,17 @@ 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 hostname = args.value_of("hostname").unwrap();
|
||||
let open_browser = args.is_present("open");
|
||||
let port = args.get_one::<String>("port").unwrap();
|
||||
let hostname = args.get_one::<String>("hostname").unwrap();
|
||||
let open_browser = args.get_flag("open");
|
||||
|
||||
let address = format!("{}:{}", hostname, port);
|
||||
let address = format!("{hostname}:{port}");
|
||||
|
||||
let update_config = |book: &mut MDBook| {
|
||||
book.config
|
||||
.set("output.html.live-reload-endpoint", &LIVE_RELOAD_ENDPOINT)
|
||||
.set("output.html.live-reload-endpoint", LIVE_RELOAD_ENDPOINT)
|
||||
.expect("live-reload-endpoint update failed");
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
// Override site-url for local serving of the 404 file
|
||||
@@ -89,8 +77,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let input_404 = book
|
||||
.config
|
||||
.get("output.html.input-404")
|
||||
.map(toml::Value::as_str)
|
||||
.and_then(std::convert::identity) // flatten
|
||||
.and_then(toml::Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
let file_404 = get_404_output_file(&input_404);
|
||||
|
||||
@@ -102,7 +89,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
serve(build_dir, sockaddr, reload_tx, &file_404);
|
||||
});
|
||||
|
||||
let serving_url = format!("http://{}", address);
|
||||
let serving_url = format!("http://{address}");
|
||||
info!("Serving on: {}", serving_url);
|
||||
|
||||
if open_browser {
|
||||
@@ -110,23 +97,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
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| {
|
||||
update_config(&mut b);
|
||||
b.build()
|
||||
});
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to load the book");
|
||||
utils::log_backtrace(&e);
|
||||
} else {
|
||||
{
|
||||
let watcher = watch::WatcherKind::from_str(args.get_one::<String>("watcher").unwrap());
|
||||
watch::rebuild_on_change(watcher, &book_dir, &update_config, &move || {
|
||||
let _ = tx.send(Message::text("reload"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let _ = thread_handle.join();
|
||||
|
||||
|
||||
@@ -1,54 +1,58 @@
|
||||
use super::command_prelude::*;
|
||||
use crate::get_book_dir;
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use clap::builder::NonEmptyStringValueParser;
|
||||
use clap::ArgAction;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("test")
|
||||
pub fn make_subcommand() -> Command {
|
||||
Command::new("test")
|
||||
.about("Tests that a book's Rust code samples compile")
|
||||
// FIXME: --dest-dir is unused by the test command, it should be removed
|
||||
.arg_dest_dir()
|
||||
.arg_root_dir()
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
Arg::new("chapter")
|
||||
.short('c')
|
||||
.long("chapter")
|
||||
.value_name("chapter"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("library-path")
|
||||
.short('L')
|
||||
.long("library-path")
|
||||
.value_name("dir")
|
||||
.value_delimiter(',')
|
||||
.value_parser(NonEmptyStringValueParser::new())
|
||||
.action(ArgAction::Append)
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
"A comma-separated list of directories to add to the crate \
|
||||
search path when building tests",
|
||||
),
|
||||
)
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(Arg::new("library-path")
|
||||
.short('L')
|
||||
.long("library-path")
|
||||
.value_name("dir")
|
||||
.takes_value(true)
|
||||
.use_delimiter(true)
|
||||
.require_delimiter(true)
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.forbid_empty_values(true)
|
||||
.help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
|
||||
}
|
||||
|
||||
// test command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let library_paths: Vec<&str> = args
|
||||
.values_of("library-path")
|
||||
.map(std::iter::Iterator::collect)
|
||||
.get_many("library-path")
|
||||
.map(|it| it.map(String::as_str).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let chapter: Option<&str> = args.get_one::<String>("chapter").map(|s| s.as_str());
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
let mut book = MDBook::load(book_dir)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.to_path_buf();
|
||||
}
|
||||
|
||||
book.test(library_paths)?;
|
||||
match chapter {
|
||||
Some(_) => book.test_chapter(library_paths, chapter),
|
||||
None => book.test(library_paths),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
171
src/cmd/watch.rs
171
src/cmd/watch.rs
@@ -1,34 +1,35 @@
|
||||
use super::command_prelude::*;
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{arg, App, Arg, ArgMatches};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use notify::Watcher;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
mod native;
|
||||
mod poller;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'help>() -> App<'help> {
|
||||
App::new("watch")
|
||||
pub fn make_subcommand() -> Command {
|
||||
Command::new("watch")
|
||||
.about("Watches a book's files and rebuilds it on changes")
|
||||
.arg(
|
||||
Arg::new("dest-dir")
|
||||
.short('d')
|
||||
.long("dest-dir")
|
||||
.value_name("dest-dir")
|
||||
.help(
|
||||
"Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
|
||||
),
|
||||
)
|
||||
.arg(arg!([dir]
|
||||
"Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)"
|
||||
))
|
||||
.arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||
.arg_dest_dir()
|
||||
.arg_root_dir()
|
||||
.arg_open()
|
||||
.arg_watcher()
|
||||
}
|
||||
|
||||
pub enum WatcherKind {
|
||||
Poll,
|
||||
Native,
|
||||
}
|
||||
|
||||
impl WatcherKind {
|
||||
pub fn from_str(s: &str) -> WatcherKind {
|
||||
match s {
|
||||
"poll" => WatcherKind::Poll,
|
||||
"native" => WatcherKind::Native,
|
||||
_ => panic!("unsupported watcher {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
@@ -37,13 +38,13 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
let update_config = |book: &mut MDBook| {
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
};
|
||||
update_config(&mut book);
|
||||
|
||||
if args.is_present("open") {
|
||||
if args.get_flag("open") {
|
||||
book.build()?;
|
||||
let path = book.build_dir_for("html").join("index.html");
|
||||
if !path.exists() {
|
||||
@@ -53,42 +54,21 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
open(path);
|
||||
}
|
||||
|
||||
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");
|
||||
utils::log_backtrace(&e);
|
||||
}
|
||||
});
|
||||
let watcher = WatcherKind::from_str(args.get_one::<String>("watcher").unwrap());
|
||||
rebuild_on_change(watcher, &book_dir, &update_config, &|| {});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_ignored_files(book_root: &Path, 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()
|
||||
}
|
||||
pub fn rebuild_on_change(
|
||||
kind: WatcherKind,
|
||||
book_dir: &Path,
|
||||
update_config: &dyn Fn(&mut MDBook),
|
||||
post_build: &dyn Fn(),
|
||||
) {
|
||||
match kind {
|
||||
WatcherKind::Poll => self::poller::rebuild_on_change(book_dir, update_config, post_build),
|
||||
WatcherKind::Native => self::native::rebuild_on_change(book_dir, update_config, post_build),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,78 +78,3 @@ fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
|
||||
.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(Vec<PathBuf>, &Path),
|
||||
{
|
||||
use notify::DebouncedEvent::*;
|
||||
use notify::RecursiveMode::*;
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
std::process::exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
|
||||
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let _ = watcher.watch(book.theme_dir(), Recursive);
|
||||
|
||||
// Add the book.toml file to the watcher if it exists
|
||||
let _ = watcher.watch(book.root.join("book.toml"), NonRecursive);
|
||||
|
||||
info!("Listening for changes...");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
189
src/cmd/watch/native.rs
Normal file
189
src/cmd/watch/native.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
//! A filesystem watcher using native operating system facilities.
|
||||
|
||||
use ignore::gitignore::Gitignore;
|
||||
use mdbook::MDBook;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn rebuild_on_change(
|
||||
book_dir: &Path,
|
||||
update_config: &dyn Fn(&mut MDBook),
|
||||
post_build: &dyn Fn(),
|
||||
) {
|
||||
use notify::RecursiveMode::*;
|
||||
|
||||
let mut book = MDBook::load(book_dir).unwrap_or_else(|e| {
|
||||
error!("failed to load book: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut debouncer = match notify_debouncer_mini::new_debouncer(Duration::from_secs(1), tx) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
std::process::exit(1)
|
||||
}
|
||||
};
|
||||
let watcher = debouncer.watcher();
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(&book.source_dir(), Recursive) {
|
||||
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let _ = watcher.watch(&book.theme_dir(), Recursive);
|
||||
|
||||
// Add the book.toml file to the watcher if it exists
|
||||
let _ = watcher.watch(&book.root.join("book.toml"), NonRecursive);
|
||||
|
||||
for dir in &book.config.build.extra_watch_dirs {
|
||||
let path = book.root.join(dir);
|
||||
let canonical_path = path.canonicalize().unwrap_or_else(|e| {
|
||||
error!("Error while watching extra directory {path:?}:\n {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
if let Err(e) = watcher.watch(&canonical_path, Recursive) {
|
||||
error!(
|
||||
"Error while watching extra directory {:?}:\n {:?}",
|
||||
canonical_path, e
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Listening for changes...");
|
||||
|
||||
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: Vec<_> = all_events
|
||||
.filter_map(|event| match event {
|
||||
Ok(events) => Some(events),
|
||||
Err(error) => {
|
||||
log::warn!("error while watching for changes: {error}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.map(|event| event.path)
|
||||
.collect();
|
||||
|
||||
// If we are watching files outside the current repository (via extra-watch-dirs), then they are definitionally
|
||||
// ignored by gitignore. So we handle this case by including such files into the watched paths list.
|
||||
let any_external_paths = paths.iter().filter(|p| !p.starts_with(&book.root)).cloned();
|
||||
let mut paths = remove_ignored_files(&book.root, &paths[..]);
|
||||
paths.extend(any_external_paths);
|
||||
|
||||
if !paths.is_empty() {
|
||||
info!("Files changed: {paths:?}");
|
||||
match MDBook::load(book_dir) {
|
||||
Ok(mut b) => {
|
||||
update_config(&mut b);
|
||||
if let Err(e) = b.build() {
|
||||
error!("failed to build the book: {e:?}");
|
||||
} else {
|
||||
post_build();
|
||||
}
|
||||
book = b;
|
||||
}
|
||||
Err(e) => error!("failed to load book config: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
if paths.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
match super::find_gitignore(book_root) {
|
||||
Some(gitignore_path) => {
|
||||
let (ignore, err) = Gitignore::new(&gitignore_path);
|
||||
if let Some(err) = err {
|
||||
warn!(
|
||||
"error reading gitignore `{}`: {err}",
|
||||
gitignore_path.display()
|
||||
);
|
||||
}
|
||||
filter_ignored_files(ignore, paths)
|
||||
}
|
||||
None => {
|
||||
// There is no .gitignore file.
|
||||
paths.iter().map(|path| path.to_path_buf()).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The usage of `canonicalize` may encounter occasional failures on the Windows platform, presenting a potential risk.
|
||||
// For more details, refer to [Pull Request #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981).
|
||||
fn filter_ignored_files(ignore: Gitignore, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
let ignore_root = ignore
|
||||
.path()
|
||||
.canonicalize()
|
||||
.expect("ignore root canonicalize error");
|
||||
|
||||
paths
|
||||
.iter()
|
||||
.filter(|path| {
|
||||
let relative_path = pathdiff::diff_paths(&path, &ignore_root)
|
||||
.expect("One of the paths should be an absolute");
|
||||
!ignore
|
||||
.matched_path_or_any_parents(&relative_path, relative_path.is_dir())
|
||||
.is_ignore()
|
||||
})
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ignore::gitignore::GitignoreBuilder;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_filter_ignored_files() {
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
|
||||
let ignore = GitignoreBuilder::new(¤t_dir)
|
||||
.add_line(None, "*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
let should_remain = current_dir.join("record.text");
|
||||
let should_filter = current_dir.join("index.html");
|
||||
|
||||
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
|
||||
assert_eq!(remain, vec![should_remain])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_ignored_files_should_handle_parent_dir() {
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
|
||||
let ignore = GitignoreBuilder::new(¤t_dir)
|
||||
.add_line(None, "*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let parent_dir = current_dir.join("..");
|
||||
let should_remain = parent_dir.join("record.text");
|
||||
let should_filter = parent_dir.join("index.html");
|
||||
|
||||
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
|
||||
assert_eq!(remain, vec![should_remain])
|
||||
}
|
||||
}
|
||||
386
src/cmd/watch/poller.rs
Normal file
386
src/cmd/watch/poller.rs
Normal file
@@ -0,0 +1,386 @@
|
||||
//! A simple poll-based filesystem watcher.
|
||||
//!
|
||||
//! This exists because the native change notifications have historically had
|
||||
//! lots of problems. Various operating systems and different filesystems have
|
||||
//! had problems correctly reporting changes.
|
||||
|
||||
use ignore::gitignore::Gitignore;
|
||||
use mdbook::MDBook;
|
||||
use pathdiff::diff_paths;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::FileType;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Calls the closure when a book source file is changed, blocking indefinitely.
|
||||
pub fn rebuild_on_change(
|
||||
book_dir: &Path,
|
||||
update_config: &dyn Fn(&mut MDBook),
|
||||
post_build: &dyn Fn(),
|
||||
) {
|
||||
let mut book = MDBook::load(book_dir).unwrap_or_else(|e| {
|
||||
error!("failed to load book: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let mut watcher = Watcher::new(book_dir);
|
||||
|
||||
info!("Watching for changes...");
|
||||
// Scan once to initialize the starting point.
|
||||
watcher.set_roots(&book);
|
||||
watcher.scan();
|
||||
|
||||
// Track average scan time, to help investigate if the poller is taking
|
||||
// undesirably long. This is not a rigorous benchmark, just a rough
|
||||
// estimate.
|
||||
const AVG_SIZE: usize = 60;
|
||||
let mut avgs = vec![0.0; AVG_SIZE];
|
||||
let mut avg_i = 0;
|
||||
|
||||
loop {
|
||||
std::thread::sleep(Duration::new(1, 0));
|
||||
watcher.set_roots(&book);
|
||||
let start = Instant::now();
|
||||
let paths = watcher.scan();
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
avgs[avg_i] = elapsed;
|
||||
avg_i += 1;
|
||||
if avg_i >= AVG_SIZE {
|
||||
avg_i = 0;
|
||||
let avg = avgs.iter().sum::<f64>() / (avgs.len() as f64);
|
||||
trace!(
|
||||
"scan average time: {avg:.2}s, scan size is {}",
|
||||
watcher.path_data.len()
|
||||
);
|
||||
}
|
||||
|
||||
if !paths.is_empty() {
|
||||
info!("Files changed: {paths:?}");
|
||||
match MDBook::load(book_dir) {
|
||||
Ok(mut b) => {
|
||||
update_config(&mut b);
|
||||
if let Err(e) = b.build() {
|
||||
error!("failed to build the book: {e:?}");
|
||||
} else {
|
||||
post_build();
|
||||
}
|
||||
book = b;
|
||||
}
|
||||
Err(e) => error!("failed to load book config: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct PathData {
|
||||
file_type: FileType,
|
||||
mtime: SystemTime,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
/// A very simple poll-watcher that scans for modified files.
|
||||
#[derive(Default)]
|
||||
struct Watcher {
|
||||
/// The root paths where it will recursively scan for changes.
|
||||
root_paths: Vec<PathBuf>,
|
||||
/// Data about files on disk.
|
||||
path_data: HashMap<PathBuf, PathData>,
|
||||
/// Filters paths that will be watched.
|
||||
ignore: Option<(PathBuf, Gitignore)>,
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
fn new(book_root: &Path) -> Watcher {
|
||||
// FIXME: ignore should be reloaded when it changes.
|
||||
let ignore = super::find_gitignore(book_root).map(|gitignore_path| {
|
||||
let (ignore, err) = Gitignore::new(&gitignore_path);
|
||||
if let Some(err) = err {
|
||||
warn!(
|
||||
"error reading gitignore `{}`: {err}",
|
||||
gitignore_path.display()
|
||||
);
|
||||
}
|
||||
// Note: The usage of `canonicalize` may encounter occasional
|
||||
// failures on the Windows platform, presenting a potential risk.
|
||||
// For more details, refer to [Pull Request
|
||||
// #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981).
|
||||
let ignore_path = ignore
|
||||
.path()
|
||||
.canonicalize()
|
||||
.expect("ignore root canonicalize error");
|
||||
(ignore_path, ignore)
|
||||
});
|
||||
|
||||
Watcher {
|
||||
ignore,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the root directories where scanning will start.
|
||||
fn set_roots(&mut self, book: &MDBook) {
|
||||
let mut root_paths = vec![
|
||||
book.source_dir(),
|
||||
book.theme_dir(),
|
||||
book.root.join("book.toml"),
|
||||
];
|
||||
root_paths.extend(
|
||||
book.config
|
||||
.build
|
||||
.extra_watch_dirs
|
||||
.iter()
|
||||
.map(|path| book.root.join(path)),
|
||||
);
|
||||
if let Some(html_config) = book.config.html_config() {
|
||||
root_paths.extend(
|
||||
html_config
|
||||
.additional_css
|
||||
.iter()
|
||||
.chain(html_config.additional_js.iter())
|
||||
.map(|path| book.root.join(path)),
|
||||
);
|
||||
}
|
||||
|
||||
self.root_paths = root_paths;
|
||||
}
|
||||
|
||||
/// Scans for changes.
|
||||
///
|
||||
/// Returns the paths that have changed.
|
||||
fn scan(&mut self) -> Vec<PathBuf> {
|
||||
let ignore = &self.ignore;
|
||||
let new_path_data: HashMap<_, _> = self
|
||||
.root_paths
|
||||
.iter()
|
||||
.filter(|root| root.exists())
|
||||
.flat_map(|root| {
|
||||
WalkDir::new(root)
|
||||
.follow_links(true)
|
||||
.into_iter()
|
||||
.filter_entry(|entry| {
|
||||
if let Some((ignore_path, ignore)) = ignore {
|
||||
let path = entry.path();
|
||||
// Canonicalization helps with removing `..` and
|
||||
// `.` entries, which can cause issues with
|
||||
// diff_paths.
|
||||
let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
let relative_path = diff_paths(&path, &ignore_path)
|
||||
.expect("One of the paths should be an absolute");
|
||||
if ignore
|
||||
.matched_path_or_any_parents(&relative_path, relative_path.is_dir())
|
||||
.is_ignore()
|
||||
{
|
||||
trace!("ignoring {path:?}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.filter_map(move |entry| {
|
||||
let entry = match entry {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
debug!("failed to scan {root:?}: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if entry.file_type().is_dir() {
|
||||
// Changes to directories themselves aren't
|
||||
// particularly interesting.
|
||||
return None;
|
||||
}
|
||||
let path = entry.path().to_path_buf();
|
||||
|
||||
let meta = match entry.metadata() {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
debug!("failed to scan {path:?}: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mtime = meta.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
let pd = PathData {
|
||||
file_type: meta.file_type(),
|
||||
mtime,
|
||||
size: meta.len(),
|
||||
};
|
||||
Some((path, pd))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut paths = Vec::new();
|
||||
for (new_path, new_data) in &new_path_data {
|
||||
match self.path_data.get(new_path) {
|
||||
Some(old_data) => {
|
||||
if new_data != old_data {
|
||||
paths.push(new_path.to_path_buf());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
paths.push(new_path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for old_path in self.path_data.keys() {
|
||||
if !new_path_data.contains_key(old_path) {
|
||||
paths.push(old_path.to_path_buf());
|
||||
}
|
||||
}
|
||||
self.path_data = new_path_data;
|
||||
paths
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Helper for testing the watcher.
|
||||
fn check_watch_behavior(
|
||||
gitignore_path: &str,
|
||||
gitignore: &str,
|
||||
book_root_path: &str,
|
||||
ignored: &[&str],
|
||||
not_ignored: &[&str],
|
||||
extra_setup: &dyn Fn(&Path),
|
||||
) {
|
||||
// Create the book and initialize things.
|
||||
let temp = tempfile::Builder::new()
|
||||
.prefix("mdbook-")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let root = temp.path();
|
||||
let book_root = root.join(book_root_path);
|
||||
// eprintln!("book_root={book_root:?}",);
|
||||
MDBook::init(&book_root).build().unwrap();
|
||||
std::fs::write(root.join(gitignore_path), gitignore).unwrap();
|
||||
let create = |paths: &[&str]| {
|
||||
let mut paths = paths
|
||||
.iter()
|
||||
.map(|path| root.join(path))
|
||||
.inspect(|path| {
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, "initial content").unwrap();
|
||||
})
|
||||
.map(|path| path.canonicalize().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
paths.sort();
|
||||
paths
|
||||
};
|
||||
let ignored = create(ignored);
|
||||
let not_ignored = create(not_ignored);
|
||||
extra_setup(&book_root);
|
||||
// Create a watcher and check its behavior.
|
||||
let book = MDBook::load(&book_root).unwrap();
|
||||
let mut watcher = Watcher::new(&book_root);
|
||||
watcher.set_roots(&book);
|
||||
// Do an initial scan to initialize its state.
|
||||
watcher.scan();
|
||||
// Verify the steady state is empty.
|
||||
let changed = watcher.scan();
|
||||
assert_eq!(changed, Vec::<PathBuf>::new());
|
||||
// Modify all files, and verify that only not_ignored are detected.
|
||||
for path in ignored.iter().chain(not_ignored.iter()) {
|
||||
std::fs::write(path, "modified").unwrap();
|
||||
}
|
||||
let changed = watcher.scan();
|
||||
let mut changed = changed
|
||||
.into_iter()
|
||||
.map(|p| p.canonicalize().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
changed.sort();
|
||||
assert_eq!(changed, not_ignored);
|
||||
// Verify again that steady state is empty.
|
||||
let changed = watcher.scan();
|
||||
assert_eq!(changed, Vec::<PathBuf>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignore() {
|
||||
// Basic gitignore test.
|
||||
check_watch_behavior(
|
||||
"foo/.gitignore",
|
||||
"*.tmp",
|
||||
"foo",
|
||||
&["foo/src/somefile.tmp"],
|
||||
&["foo/src/chapter.md"],
|
||||
&|_book_root| {},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignore_in_parent() {
|
||||
// gitignore is in the parent of the book
|
||||
check_watch_behavior(
|
||||
".gitignore",
|
||||
"*.tmp\nsomedir/\n/inroot\n/foo/src/inbook\n",
|
||||
"foo",
|
||||
&[
|
||||
"foo/src/somefile.tmp",
|
||||
"foo/src/somedir/somefile",
|
||||
"inroot/somefile",
|
||||
"foo/src/inbook/somefile",
|
||||
],
|
||||
&["foo/src/inroot/somefile"],
|
||||
&|_book_root| {},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignore_canonical() {
|
||||
// test with path with ..
|
||||
check_watch_behavior(
|
||||
".gitignore",
|
||||
"*.tmp\nsomedir/\n/foo/src/inbook\n",
|
||||
"bar/../foo",
|
||||
&[
|
||||
"foo/src/somefile.tmp",
|
||||
"foo/src/somedir/somefile",
|
||||
"foo/src/inbook/somefile",
|
||||
],
|
||||
&["foo/src/chapter.md"],
|
||||
&|_book_root| {},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scan_extra_watch() {
|
||||
// Check behavior with extra-watch-dirs
|
||||
check_watch_behavior(
|
||||
".gitignore",
|
||||
"*.tmp\n/outside-root/ignoreme\n/foo/examples/ignoreme\n",
|
||||
"foo",
|
||||
&[
|
||||
"foo/src/somefile.tmp",
|
||||
"foo/examples/example.tmp",
|
||||
"outside-root/somefile.tmp",
|
||||
"outside-root/ignoreme",
|
||||
"foo/examples/ignoreme",
|
||||
],
|
||||
&[
|
||||
"foo/src/chapter.md",
|
||||
"foo/examples/example.rs",
|
||||
"foo/examples/example2.rs",
|
||||
"outside-root/image.png",
|
||||
],
|
||||
&|book_root| {
|
||||
std::fs::write(
|
||||
book_root.join("book.toml"),
|
||||
r#"
|
||||
[book]
|
||||
title = "foo"
|
||||
|
||||
[build]
|
||||
extra-watch-dirs = [
|
||||
"examples",
|
||||
"../outside-root",
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
199
src/config.rs
199
src/config.rs
@@ -49,6 +49,7 @@
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use log::{debug, trace, warn};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
@@ -57,7 +58,7 @@ use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use toml::value::Table;
|
||||
use toml::{self, Value};
|
||||
use toml::Value;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::utils::{self, toml_ext::TomlExt};
|
||||
@@ -144,7 +145,7 @@ impl Config {
|
||||
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);
|
||||
let full_key = format!("{key}.{k}");
|
||||
self.set(&full_key, v).expect("unreachable");
|
||||
}
|
||||
return;
|
||||
@@ -295,7 +296,7 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
impl<'de> serde::Deserialize<'de> for Config {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
|
||||
let raw = Value::deserialize(de)?;
|
||||
|
||||
@@ -307,7 +308,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.github.io/mdBook/format/config.html");
|
||||
warn!("Documentation: https://rust-lang.github.io/mdBook/format/config.html");
|
||||
return Ok(Config::from_legacy(raw));
|
||||
}
|
||||
|
||||
@@ -410,6 +411,9 @@ pub struct BookConfig {
|
||||
pub multilingual: bool,
|
||||
/// The main language of the book.
|
||||
pub language: Option<String>,
|
||||
/// The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL).
|
||||
/// When not specified, the text direction is derived from [`BookConfig::language`].
|
||||
pub text_direction: Option<TextDirection>,
|
||||
}
|
||||
|
||||
impl Default for BookConfig {
|
||||
@@ -421,6 +425,43 @@ impl Default for BookConfig {
|
||||
src: PathBuf::from("src"),
|
||||
multilingual: false,
|
||||
language: Some(String::from("en")),
|
||||
text_direction: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BookConfig {
|
||||
/// Gets the realized text direction, either from [`BookConfig::text_direction`]
|
||||
/// or derived from [`BookConfig::language`], to be used by templating engines.
|
||||
pub fn realized_text_direction(&self) -> TextDirection {
|
||||
if let Some(direction) = self.text_direction {
|
||||
direction
|
||||
} else {
|
||||
TextDirection::from_lang_code(self.language.as_deref().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Text direction to use for HTML output
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TextDirection {
|
||||
/// Left to right.
|
||||
#[serde(rename = "ltr")]
|
||||
LeftToRight,
|
||||
/// Right to left
|
||||
#[serde(rename = "rtl")]
|
||||
RightToLeft,
|
||||
}
|
||||
|
||||
impl TextDirection {
|
||||
/// Gets the text direction from language code
|
||||
pub fn from_lang_code(code: &str) -> Self {
|
||||
match code {
|
||||
// list sourced from here: https://github.com/abarrak/rtl/blob/master/lib/rtl/core.rb#L16
|
||||
"ar" | "ara" | "arc" | "ae" | "ave" | "egy" | "he" | "heb" | "nqo" | "pal" | "phn"
|
||||
| "sam" | "syc" | "syr" | "fa" | "per" | "fas" | "ku" | "kur" | "ur" | "urd"
|
||||
| "pus" | "ps" | "yi" | "yid" => TextDirection::RightToLeft,
|
||||
_ => TextDirection::LeftToRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,6 +478,8 @@ pub struct BuildConfig {
|
||||
/// Should the default preprocessors always be used when they are
|
||||
/// compatible with the renderer?
|
||||
pub use_default_preprocessors: bool,
|
||||
/// Extra directories to trigger rebuild when watching/serving
|
||||
pub extra_watch_dirs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for BuildConfig {
|
||||
@@ -445,6 +488,7 @@ impl Default for BuildConfig {
|
||||
build_dir: PathBuf::from("book"),
|
||||
create_missing: true,
|
||||
use_default_preprocessors: true,
|
||||
extra_watch_dirs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,6 +504,9 @@ pub struct RustConfig {
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// Rust edition to use for the code.
|
||||
pub enum RustEdition {
|
||||
/// The 2024 edition of Rust
|
||||
#[serde(rename = "2024")]
|
||||
E2024,
|
||||
/// The 2021 edition of Rust
|
||||
#[serde(rename = "2021")]
|
||||
E2021,
|
||||
@@ -482,7 +529,9 @@ pub struct HtmlConfig {
|
||||
/// 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.
|
||||
/// Supports smart quotes, apostrophes, ellipsis, en-dash, and em-dash.
|
||||
pub smart_punctuation: bool,
|
||||
/// Deprecated alias for `smart_punctuation`.
|
||||
pub curly_quotes: bool,
|
||||
/// Should mathjax be enabled?
|
||||
pub mathjax_support: bool,
|
||||
@@ -500,6 +549,8 @@ pub struct HtmlConfig {
|
||||
/// Playground settings.
|
||||
#[serde(alias = "playpen")]
|
||||
pub playground: Playground,
|
||||
/// Code settings.
|
||||
pub code: Code,
|
||||
/// Print settings.
|
||||
pub print: Print,
|
||||
/// Don't render section labels.
|
||||
@@ -526,10 +577,9 @@ pub struct HtmlConfig {
|
||||
/// directly jumping to editing the currently viewed page.
|
||||
/// Contains {path} that is replaced with chapter source file path
|
||||
pub edit_url_template: Option<String>,
|
||||
/// Endpoint of websocket, for livereload usage. Value loaded from .toml file
|
||||
/// is ignored, because our code overrides this field with the value [`LIVE_RELOAD_ENDPOINT`]
|
||||
///
|
||||
/// [`LIVE_RELOAD_ENDPOINT`]: cmd::serve::LIVE_RELOAD_ENDPOINT
|
||||
/// Endpoint of websocket, for livereload usage. Value loaded from .toml
|
||||
/// file is ignored, because our code overrides this field with an
|
||||
/// internal value (`LIVE_RELOAD_ENDPOINT)
|
||||
///
|
||||
/// This config item *should not be edited* by the end user.
|
||||
#[doc(hidden)]
|
||||
@@ -545,6 +595,7 @@ impl Default for HtmlConfig {
|
||||
theme: None,
|
||||
default_theme: None,
|
||||
preferred_dark_theme: None,
|
||||
smart_punctuation: false,
|
||||
curly_quotes: false,
|
||||
mathjax_support: false,
|
||||
copy_fonts: true,
|
||||
@@ -553,6 +604,7 @@ impl Default for HtmlConfig {
|
||||
additional_js: Vec::new(),
|
||||
fold: Fold::default(),
|
||||
playground: Playground::default(),
|
||||
code: Code::default(),
|
||||
print: Print::default(),
|
||||
no_section_label: false,
|
||||
search: None,
|
||||
@@ -577,6 +629,11 @@ impl HtmlConfig {
|
||||
None => root.join("theme"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if smart punctuation is enabled.
|
||||
pub fn smart_punctuation(&self) -> bool {
|
||||
self.smart_punctuation || self.curly_quotes
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for how to render the print icon, print.html, and print.css.
|
||||
@@ -610,7 +667,7 @@ pub struct Fold {
|
||||
pub level: u8,
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playground.
|
||||
/// Configuration for tweaking how the HTML renderer handles the playground.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Playground {
|
||||
@@ -639,6 +696,14 @@ impl Default for Playground {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the HTML renderer handles code blocks.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Code {
|
||||
/// A prefix string to hide lines per language (one or more chars).
|
||||
pub hidelines: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Configuration of the search functionality of the HTML renderer.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
@@ -700,7 +765,7 @@ trait Updateable<'de>: Serialize + Deserialize<'de> {
|
||||
let mut raw = Value::try_from(&self).expect("unreachable");
|
||||
|
||||
if let Ok(value) = Value::try_from(value) {
|
||||
let _ = raw.insert(key, value);
|
||||
raw.insert(key, value);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -717,6 +782,7 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::fs::get_404_output_file;
|
||||
use serde_json::json;
|
||||
|
||||
const COMPLEX_CONFIG: &str = r#"
|
||||
[book]
|
||||
@@ -735,7 +801,7 @@ mod tests {
|
||||
[output.html]
|
||||
theme = "./themedir"
|
||||
default-theme = "rust"
|
||||
curly-quotes = true
|
||||
smart-punctuation = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["./foo/bar/baz.css"]
|
||||
git-repository-url = "https://foo.com/"
|
||||
@@ -765,11 +831,13 @@ mod tests {
|
||||
multilingual: true,
|
||||
src: PathBuf::from("source"),
|
||||
language: Some(String::from("ja")),
|
||||
text_direction: None,
|
||||
};
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("outputs"),
|
||||
create_missing: false,
|
||||
use_default_preprocessors: true,
|
||||
extra_watch_dirs: Vec::new(),
|
||||
};
|
||||
let rust_should_be = RustConfig { edition: None };
|
||||
let playground_should_be = Playground {
|
||||
@@ -780,7 +848,7 @@ mod tests {
|
||||
runnable: true,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
smart_punctuation: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
||||
theme: Some(PathBuf::from("./themedir")),
|
||||
@@ -960,7 +1028,7 @@ mod tests {
|
||||
[output.html]
|
||||
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
|
||||
theme = "my-theme"
|
||||
curly-quotes = true
|
||||
smart-punctuation = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
@@ -980,11 +1048,12 @@ mod tests {
|
||||
build_dir: PathBuf::from("my-book"),
|
||||
create_missing: true,
|
||||
use_default_preprocessors: true,
|
||||
extra_watch_dirs: Vec::new(),
|
||||
};
|
||||
|
||||
let html_should_be = HtmlConfig {
|
||||
theme: Some(PathBuf::from("my-theme")),
|
||||
curly_quotes: true,
|
||||
smart_punctuation: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
|
||||
additional_js: vec![PathBuf::from("custom.js")],
|
||||
@@ -1115,6 +1184,73 @@ mod tests {
|
||||
assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_direction_ltr() {
|
||||
let src = r#"
|
||||
[book]
|
||||
text-direction = "ltr"
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book.text_direction, Some(TextDirection::LeftToRight));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_direction_rtl() {
|
||||
let src = r#"
|
||||
[book]
|
||||
text-direction = "rtl"
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book.text_direction, Some(TextDirection::RightToLeft));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_direction_none() {
|
||||
let src = r#"
|
||||
[book]
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book.text_direction, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_direction() {
|
||||
let mut cfg = BookConfig::default();
|
||||
|
||||
// test deriving the text direction from language codes
|
||||
cfg.language = Some("ar".into());
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
|
||||
|
||||
cfg.language = Some("he".into());
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
|
||||
|
||||
cfg.language = Some("en".into());
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
|
||||
|
||||
cfg.language = Some("ja".into());
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
|
||||
|
||||
// test forced direction
|
||||
cfg.language = Some("ar".into());
|
||||
cfg.text_direction = Some(TextDirection::LeftToRight);
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
|
||||
|
||||
cfg.language = Some("ar".into());
|
||||
cfg.text_direction = Some(TextDirection::RightToLeft);
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
|
||||
|
||||
cfg.language = Some("en".into());
|
||||
cfg.text_direction = Some(TextDirection::LeftToRight);
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
|
||||
|
||||
cfg.language = Some("en".into());
|
||||
cfg.text_direction = Some(TextDirection::RightToLeft);
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Invalid configuration file")]
|
||||
fn invalid_language_type_error() {
|
||||
@@ -1187,4 +1323,37 @@ mod tests {
|
||||
assert!(html_config.print.enable);
|
||||
assert!(!html_config.print.page_break);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curly_quotes_or_smart_punctuation() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
|
||||
[output.html]
|
||||
smart-punctuation = true
|
||||
"#;
|
||||
let config = Config::from_str(src).unwrap();
|
||||
assert_eq!(config.html_config().unwrap().smart_punctuation(), true);
|
||||
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
|
||||
[output.html]
|
||||
curly-quotes = true
|
||||
"#;
|
||||
let config = Config::from_str(src).unwrap();
|
||||
assert_eq!(config.html_config().unwrap().smart_punctuation(), true);
|
||||
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
"#;
|
||||
let config = Config::from_str(src).unwrap();
|
||||
assert_eq!(
|
||||
config.html_config().unwrap_or_default().smart_punctuation(),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
11
src/lib.rs
11
src/lib.rs
@@ -83,17 +83,6 @@
|
||||
#![deny(missing_docs)]
|
||||
#![deny(rust_2018_idioms)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
|
||||
pub mod book;
|
||||
pub mod config;
|
||||
pub mod preprocess;
|
||||
|
||||
42
src/main.rs
42
src/main.rs
@@ -5,7 +5,7 @@ extern crate log;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::Local;
|
||||
use clap::{App, AppSettings, Arg, ArgMatches};
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use clap_complete::Shell;
|
||||
use env_logger::Builder;
|
||||
use log::LevelFilter;
|
||||
@@ -13,7 +13,7 @@ use mdbook::utils;
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod cmd;
|
||||
|
||||
@@ -22,10 +22,10 @@ const VERSION: &str = concat!("v", crate_version!());
|
||||
fn main() {
|
||||
init_logger();
|
||||
|
||||
let app = create_clap_app();
|
||||
let command = create_clap_command();
|
||||
|
||||
// Check which subcomamnd the user ran...
|
||||
let res = match app.get_matches().subcommand() {
|
||||
// Check which subcommand the user ran...
|
||||
let res = match command.get_matches().subcommand() {
|
||||
Some(("init", sub_matches)) => cmd::init::execute(sub_matches),
|
||||
Some(("build", sub_matches)) => cmd::build::execute(sub_matches),
|
||||
Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches),
|
||||
@@ -35,15 +35,13 @@ fn main() {
|
||||
Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches),
|
||||
Some(("test", sub_matches)) => cmd::test::execute(sub_matches),
|
||||
Some(("completions", sub_matches)) => (|| {
|
||||
let shell: Shell = sub_matches
|
||||
.value_of("shell")
|
||||
.ok_or_else(|| anyhow!("Shell name missing."))?
|
||||
.parse()
|
||||
.map_err(|s| anyhow!("Invalid shell: {}", s))?;
|
||||
let shell = sub_matches
|
||||
.get_one::<Shell>("shell")
|
||||
.ok_or_else(|| anyhow!("Shell name missing."))?;
|
||||
|
||||
let mut complete_app = create_clap_app();
|
||||
let mut complete_app = create_clap_command();
|
||||
clap_complete::generate(
|
||||
shell,
|
||||
*shell,
|
||||
&mut complete_app,
|
||||
"mdbook",
|
||||
&mut std::io::stdout().lock(),
|
||||
@@ -61,13 +59,13 @@ fn main() {
|
||||
}
|
||||
|
||||
/// Create a list of valid arguments and sub-commands
|
||||
fn create_clap_app() -> App<'static> {
|
||||
let app = App::new(crate_name!())
|
||||
fn create_clap_command() -> Command {
|
||||
let app = Command::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
.version(VERSION)
|
||||
.setting(AppSettings::PropagateVersion)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.propagate_version(true)
|
||||
.arg_required_else_help(true)
|
||||
.after_help(
|
||||
"For more information about a specific command, try `mdbook <command> --help`\n\
|
||||
The source code for mdBook is available at: https://github.com/rust-lang/mdBook",
|
||||
@@ -77,12 +75,11 @@ fn create_clap_app() -> App<'static> {
|
||||
.subcommand(cmd::test::make_subcommand())
|
||||
.subcommand(cmd::clean::make_subcommand())
|
||||
.subcommand(
|
||||
App::new("completions")
|
||||
Command::new("completions")
|
||||
.about("Generate shell completions for your shell to stdout")
|
||||
.arg(
|
||||
Arg::new("shell")
|
||||
.takes_value(true)
|
||||
.possible_values(Shell::possible_values())
|
||||
.value_parser(clap::value_parser!(Shell))
|
||||
.help("the shell to generate completions for")
|
||||
.value_name("SHELL")
|
||||
.required(true),
|
||||
@@ -124,11 +121,10 @@ fn init_logger() {
|
||||
}
|
||||
|
||||
fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||
if let Some(dir) = args.value_of("dir") {
|
||||
if let Some(p) = args.get_one::<PathBuf>("dir") {
|
||||
// Check if path is relative from current dir, or absolute...
|
||||
let p = Path::new(dir);
|
||||
if p.is_relative() {
|
||||
env::current_dir().unwrap().join(dir)
|
||||
env::current_dir().unwrap().join(p)
|
||||
} else {
|
||||
p.to_path_buf()
|
||||
}
|
||||
@@ -146,5 +142,5 @@ fn open<P: AsRef<OsStr>>(path: P) {
|
||||
|
||||
#[test]
|
||||
fn verify_app() {
|
||||
create_clap_app().debug_assert();
|
||||
create_clap_command().debug_assert();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::Book;
|
||||
use crate::errors::*;
|
||||
use log::{debug, trace, warn};
|
||||
use shlex::Shlex;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::errors::*;
|
||||
use log::warn;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||
/// `README.md` is the de facto index file in markdown-based documentation.
|
||||
@@ -67,9 +68,8 @@ fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
||||
}
|
||||
|
||||
fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"(?i)^readme$").unwrap();
|
||||
}
|
||||
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)^readme$").unwrap());
|
||||
|
||||
RE.is_match(
|
||||
path.as_ref()
|
||||
.file_stem()
|
||||
|
||||
@@ -10,6 +10,8 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use log::{error, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
@@ -91,7 +93,7 @@ where
|
||||
for link in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..link.start_index]);
|
||||
|
||||
match link.render_with_path(&path, chapter_title) {
|
||||
match link.render_with_path(path, chapter_title) {
|
||||
Ok(new_content) => {
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = link.link_type.relative_path(path) {
|
||||
@@ -325,7 +327,7 @@ impl<'a> Link<'a> {
|
||||
let base = base.as_ref();
|
||||
match self.link_type {
|
||||
// omit the escape char
|
||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||
LinkType::Escaped => Ok(self.link_text[1..].to_owned()),
|
||||
LinkType::Include(ref pat, ref range_or_anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
@@ -408,19 +410,20 @@ impl<'a> Iterator for LinkIter<'a> {
|
||||
fn find_links(contents: &str) -> LinkIter<'_> {
|
||||
// lazily compute following regex
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(
|
||||
static RE: Lazy<Regex> = Lazy::new(|| {
|
||||
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
|
||||
([^}]+) # link target path and space separated properties
|
||||
\}\} # link closing parens"
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9_]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([^}]+) # link target path and space separated properties
|
||||
\}\} # link closing parens",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
LinkIter(RE.captures_iter(contents))
|
||||
}
|
||||
|
||||
@@ -490,7 +493,7 @@ mod tests {
|
||||
let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@@ -516,7 +519,7 @@ mod tests {
|
||||
let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@@ -533,7 +536,7 @@ mod tests {
|
||||
fn test_find_links_with_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:20}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -552,7 +555,7 @@ mod tests {
|
||||
fn test_find_links_with_line_number() {
|
||||
let s = "Some random text with {{#include file.rs:10}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -571,7 +574,7 @@ mod tests {
|
||||
fn test_find_links_with_from_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -590,7 +593,7 @@ mod tests {
|
||||
fn test_find_links_with_to_range() {
|
||||
let s = "Some random text with {{#include file.rs::20}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -609,7 +612,7 @@ mod tests {
|
||||
fn test_find_links_with_full_range() {
|
||||
let s = "Some random text with {{#include file.rs::}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -628,7 +631,7 @@ mod tests {
|
||||
fn test_find_links_with_no_range_specified() {
|
||||
let s = "Some random text with {{#include file.rs}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -647,7 +650,7 @@ mod tests {
|
||||
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);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -667,7 +670,7 @@ mod tests {
|
||||
let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@@ -687,7 +690,7 @@ mod tests {
|
||||
more\n text {{#playground my.rs editable no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
@@ -718,7 +721,7 @@ mod tests {
|
||||
no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(res.len(), 3);
|
||||
assert_eq!(
|
||||
res[0],
|
||||
|
||||
@@ -12,12 +12,11 @@ use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Extra information for a `Preprocessor` to give them more context when
|
||||
/// processing a book.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
@@ -14,7 +14,10 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::utils::fs::get_404_output_file;
|
||||
use handlebars::Handlebars;
|
||||
use log::{debug, trace, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Captures, Regex};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HtmlHandlebars;
|
||||
@@ -51,11 +54,13 @@ impl HtmlHandlebars {
|
||||
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
|
||||
}
|
||||
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
let content = utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation());
|
||||
|
||||
let fixed_content =
|
||||
utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
|
||||
let fixed_content = utils::render_markdown_with_path(
|
||||
&ch.content,
|
||||
ctx.html_config.smart_punctuation(),
|
||||
Some(path),
|
||||
);
|
||||
if !ctx.is_index && ctx.html_config.print.page_break {
|
||||
// Add page break between chapters
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
|
||||
@@ -96,7 +101,7 @@ impl HtmlHandlebars {
|
||||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&path)),
|
||||
json!(utils::fs::path_to_root(path)),
|
||||
);
|
||||
if let Some(ref section) = ch.number {
|
||||
ctx.data
|
||||
@@ -107,7 +112,12 @@ impl HtmlHandlebars {
|
||||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
let rendered = self.post_process(rendered, &ctx.html_config.playground, ctx.edition);
|
||||
let rendered = self.post_process(
|
||||
rendered,
|
||||
&ctx.html_config.playground,
|
||||
&ctx.html_config.code,
|
||||
ctx.edition,
|
||||
);
|
||||
|
||||
// Write to file
|
||||
debug!("Creating {}", filepath.display());
|
||||
@@ -118,8 +128,12 @@ impl HtmlHandlebars {
|
||||
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);
|
||||
let rendered_index = self.post_process(
|
||||
rendered_index,
|
||||
&ctx.html_config.playground,
|
||||
&ctx.html_config.code,
|
||||
ctx.edition,
|
||||
);
|
||||
debug!("Creating index.html from {}", ctx_path);
|
||||
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
|
||||
}
|
||||
@@ -139,13 +153,13 @@ impl HtmlHandlebars {
|
||||
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))?
|
||||
.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)
|
||||
format!("unable to open 404 input file {default_404_location:?}")
|
||||
})?
|
||||
} else {
|
||||
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
|
||||
@@ -153,7 +167,8 @@ impl HtmlHandlebars {
|
||||
.to_string()
|
||||
}
|
||||
};
|
||||
let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes);
|
||||
let html_content_404 =
|
||||
utils::render_markdown(&content_404, html_config.smart_punctuation());
|
||||
|
||||
let mut data_404 = data.clone();
|
||||
let base_url = if let Some(site_url) = &html_config.site_url {
|
||||
@@ -179,24 +194,30 @@ impl HtmlHandlebars {
|
||||
data_404.insert("title".to_owned(), json!(title));
|
||||
let rendered = handlebars.render("index", &data_404)?;
|
||||
|
||||
let rendered =
|
||||
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||
let rendered = self.post_process(
|
||||
rendered,
|
||||
&html_config.playground,
|
||||
&html_config.code,
|
||||
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))]
|
||||
#[allow(clippy::let_and_return)]
|
||||
fn post_process(
|
||||
&self,
|
||||
rendered: String,
|
||||
playground_config: &Playground,
|
||||
code_config: &Code,
|
||||
edition: Option<RustEdition>,
|
||||
) -> String {
|
||||
let rendered = build_header_links(&rendered);
|
||||
let rendered = fix_code_blocks(&rendered);
|
||||
let rendered = add_playground_pre(&rendered, playground_config, edition);
|
||||
let rendered = hide_lines(&rendered, code_config);
|
||||
|
||||
rendered
|
||||
}
|
||||
@@ -216,7 +237,7 @@ impl HtmlHandlebars {
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?;
|
||||
write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
|
||||
}
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
@@ -272,7 +293,8 @@ impl HtmlHandlebars {
|
||||
"FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
if html_config.copy_fonts {
|
||||
// Don't copy the stock fonts if the user has specified their own fonts to use.
|
||||
if html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
@@ -286,6 +308,24 @@ impl HtmlHandlebars {
|
||||
theme::fonts::SOURCE_CODE_PRO.1,
|
||||
)?;
|
||||
}
|
||||
if let Some(fonts_css) = &theme.fonts_css {
|
||||
if !fonts_css.is_empty() {
|
||||
write_file(destination, "fonts/fonts.css", fonts_css)?;
|
||||
}
|
||||
}
|
||||
if !html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
warn!(
|
||||
"output.html.copy-fonts is deprecated.\n\
|
||||
This book appears to have copy-fonts=false in book.toml without a fonts.css file.\n\
|
||||
Add an empty `theme/fonts/fonts.css` file to squelch this warning."
|
||||
);
|
||||
}
|
||||
for font_file in &theme.font_files {
|
||||
let contents = fs::read(font_file)?;
|
||||
let filename = font_file.file_name().unwrap();
|
||||
let filename = Path::new("fonts").join(filename);
|
||||
write_file(destination, filename, &contents)?;
|
||||
}
|
||||
|
||||
let playground_config = &html_config.playground;
|
||||
|
||||
@@ -337,6 +377,7 @@ impl HtmlHandlebars {
|
||||
);
|
||||
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
||||
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
||||
// TODO: remove theme_option in 0.5, it is not needed.
|
||||
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
|
||||
}
|
||||
|
||||
@@ -440,25 +481,6 @@ impl HtmlHandlebars {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mattico): Remove some time after the 0.1.8 release
|
||||
fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
|
||||
fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
|
||||
Ok(entry.file_type()?.is_file()
|
||||
&& entry.path().extension().map_or(false, |ext| ext == "md"))
|
||||
}
|
||||
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
if entry_is_maybe_book_file(entry?).unwrap_or(false) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn name(&self) -> &str {
|
||||
"html"
|
||||
@@ -491,16 +513,6 @@ impl Renderer for HtmlHandlebars {
|
||||
None => ctx.root.join("theme"),
|
||||
};
|
||||
|
||||
if html_config.theme.is_none()
|
||||
&& maybe_wrong_theme_dir(&src_dir.join("theme")).unwrap_or(false)
|
||||
{
|
||||
warn!(
|
||||
"Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
|
||||
theme directory"
|
||||
);
|
||||
warn!("Please move your theme files to `./theme` for them to continue being used");
|
||||
}
|
||||
|
||||
let theme = theme::Theme::new(theme_dir);
|
||||
|
||||
debug!("Register the index handlebars template");
|
||||
@@ -516,6 +528,11 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Register the header handlebars template");
|
||||
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
|
||||
|
||||
debug!("Register the toc handlebars template");
|
||||
handlebars.register_template_string("toc_js", String::from_utf8(theme.toc_js.clone())?)?;
|
||||
handlebars
|
||||
.register_template_string("toc_html", String::from_utf8(theme.toc_html.clone())?)?;
|
||||
|
||||
debug!("Register handlebars helpers");
|
||||
self.register_hbs_helpers(&mut handlebars, &html_config);
|
||||
|
||||
@@ -524,7 +541,7 @@ impl Renderer for HtmlHandlebars {
|
||||
// Print version
|
||||
let mut print_content = String::new();
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
fs::create_dir_all(destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
let mut is_index = true;
|
||||
@@ -560,13 +577,29 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Render template");
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
|
||||
let rendered =
|
||||
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||
let rendered = self.post_process(
|
||||
rendered,
|
||||
&html_config.playground,
|
||||
&html_config.code,
|
||||
ctx.config.rust.edition,
|
||||
);
|
||||
|
||||
utils::fs::write_file(destination, "print.html", rendered.as_bytes())?;
|
||||
debug!("Creating print.html ✓");
|
||||
}
|
||||
|
||||
debug!("Render toc");
|
||||
{
|
||||
let rendered_toc = handlebars.render("toc_js", &data)?;
|
||||
utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.js ✓");
|
||||
data.insert("is_toc_html".to_owned(), json!(true));
|
||||
let rendered_toc = handlebars.render("toc_html", &data)?;
|
||||
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.html ✓");
|
||||
data.remove("is_toc_html");
|
||||
}
|
||||
|
||||
debug!("Copy static files");
|
||||
self.copy_static_files(destination, &theme, &html_config)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
@@ -606,6 +639,10 @@ fn make_data(
|
||||
"language".to_owned(),
|
||||
json!(config.book.language.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert(
|
||||
"text_direction".to_owned(),
|
||||
json!(config.book.realized_text_direction()),
|
||||
);
|
||||
data.insert(
|
||||
"book_title".to_owned(),
|
||||
json!(config.book.title.clone().unwrap_or_default()),
|
||||
@@ -627,6 +664,7 @@ fn make_data(
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: remove default_theme in 0.5, it is not needed.
|
||||
let default_theme = match html_config.default_theme {
|
||||
Some(ref theme) => theme.to_lowercase(),
|
||||
None => "light".to_string(),
|
||||
@@ -651,7 +689,8 @@ fn make_data(
|
||||
data.insert("mathjax_support".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
if html_config.copy_fonts {
|
||||
// This `matches!` checks for a non-empty file.
|
||||
if html_config.copy_fonts || matches!(theme.fonts_css.as_deref(), Some([_, ..])) {
|
||||
data.insert("copy_fonts".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
@@ -764,9 +803,10 @@ fn make_data(
|
||||
/// Goes through the rendered HTML, making sure all header tags have
|
||||
/// an anchor respectively so people can link to sections directly.
|
||||
fn build_header_links(html: &str) -> String {
|
||||
lazy_static! {
|
||||
static ref BUILD_HEADER_LINKS: Regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
}
|
||||
static BUILD_HEADER_LINKS: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r#"<h(\d)(?: id="([^"]+)")?(?: class="([^"]+)")?>(.*?)</h\d>"#).unwrap()
|
||||
});
|
||||
static IGNORE_CLASS: &[&str] = &["menu-title"];
|
||||
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
@@ -776,7 +816,22 @@ fn build_header_links(html: &str) -> String {
|
||||
.parse()
|
||||
.expect("Regex should ensure we only ever get numbers here");
|
||||
|
||||
insert_link_into_header(level, &caps[2], &mut id_counter)
|
||||
// Ignore .menu-title because now it's getting detected by the regex.
|
||||
if let Some(classes) = caps.get(3) {
|
||||
for class in classes.as_str().split(" ") {
|
||||
if IGNORE_CLASS.contains(&class) {
|
||||
return caps[0].to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insert_link_into_header(
|
||||
level,
|
||||
&caps[4],
|
||||
caps.get(2).map(|x| x.as_str().to_string()),
|
||||
caps.get(3).map(|x| x.as_str().to_string()),
|
||||
&mut id_counter,
|
||||
)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
@@ -786,15 +841,17 @@ fn build_header_links(html: &str) -> String {
|
||||
fn insert_link_into_header(
|
||||
level: usize,
|
||||
content: &str,
|
||||
id: Option<String>,
|
||||
classes: Option<String>,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
) -> String {
|
||||
let id = utils::unique_id_from_content(content, id_counter);
|
||||
let id = id.unwrap_or_else(|| utils::unique_id_from_content(content, id_counter));
|
||||
let classes = classes
|
||||
.map(|s| format!(" class=\"{s}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
format!(
|
||||
r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content
|
||||
r##"<h{level} id="{id}"{classes}><a class="header" href="#{id}">{content}</a></h{level}>"##
|
||||
)
|
||||
}
|
||||
|
||||
@@ -807,10 +864,8 @@ fn insert_link_into_header(
|
||||
// ```
|
||||
// This function replaces all commas by spaces in the code block classes
|
||||
fn fix_code_blocks(html: &str) -> String {
|
||||
lazy_static! {
|
||||
static ref FIX_CODE_BLOCKS: Regex =
|
||||
Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
}
|
||||
static FIX_CODE_BLOCKS: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap());
|
||||
|
||||
FIX_CODE_BLOCKS
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
@@ -818,78 +873,69 @@ fn fix_code_blocks(html: &str) -> String {
|
||||
let classes = &caps[2].replace(',', " ");
|
||||
let after = &caps[3];
|
||||
|
||||
format!(
|
||||
r#"<code{before}class="{classes}"{after}>"#,
|
||||
before = before,
|
||||
classes = classes,
|
||||
after = after
|
||||
)
|
||||
format!(r#"<code{before}class="{classes}"{after}>"#)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
static CODE_BLOCK_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
|
||||
|
||||
fn add_playground_pre(
|
||||
html: &str,
|
||||
playground_config: &Playground,
|
||||
edition: Option<RustEdition>,
|
||||
) -> String {
|
||||
lazy_static! {
|
||||
static ref ADD_PLAYGROUND_PRE: Regex =
|
||||
Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
}
|
||||
ADD_PLAYGROUND_PRE
|
||||
CODE_BLOCK_RE
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if classes.contains("language-rust") {
|
||||
if (!classes.contains("ignore")
|
||||
if classes.contains("language-rust")
|
||||
&& ((!classes.contains("ignore")
|
||||
&& !classes.contains("noplayground")
|
||||
&& !classes.contains("noplaypen")
|
||||
&& playground_config.runnable)
|
||||
|| classes.contains("mdbook-runnable")
|
||||
{
|
||||
let contains_e2015 = classes.contains("edition2015");
|
||||
let contains_e2018 = classes.contains("edition2018");
|
||||
let contains_e2021 = classes.contains("edition2021");
|
||||
let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
|
||||
// the user forced edition, we should not overwrite it
|
||||
""
|
||||
} else {
|
||||
match edition {
|
||||
Some(RustEdition::E2015) => " edition2015",
|
||||
Some(RustEdition::E2018) => " edition2018",
|
||||
Some(RustEdition::E2021) => " edition2021",
|
||||
None => "",
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the contents in an external pre block
|
||||
format!(
|
||||
"<pre class=\"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!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
|
||||
.into()
|
||||
};
|
||||
hide_lines(&content)
|
||||
}
|
||||
)
|
||||
|| classes.contains("mdbook-runnable"))
|
||||
{
|
||||
let contains_e2015 = classes.contains("edition2015");
|
||||
let contains_e2018 = classes.contains("edition2018");
|
||||
let contains_e2021 = classes.contains("edition2021");
|
||||
let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
|
||||
// the user forced edition, we should not overwrite it
|
||||
""
|
||||
} else {
|
||||
format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
|
||||
}
|
||||
match edition {
|
||||
Some(RustEdition::E2015) => " edition2015",
|
||||
Some(RustEdition::E2018) => " edition2018",
|
||||
Some(RustEdition::E2021) => " edition2021",
|
||||
Some(RustEdition::E2024) => " edition2024",
|
||||
None => "",
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the contents in an external pre block
|
||||
format!(
|
||||
"<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!("# #![allow(unused)]\n{attrs}#fn main() {{\n{code}#}}").into()
|
||||
};
|
||||
content
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
@@ -898,19 +944,64 @@ fn add_playground_pre(
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn hide_lines(content: &str) -> String {
|
||||
lazy_static! {
|
||||
static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
|
||||
}
|
||||
/// Modifies all `<code>` blocks to convert "hidden" lines and to wrap them in
|
||||
/// a `<span class="boring">`.
|
||||
fn hide_lines(html: &str, code_config: &Code) -> String {
|
||||
static LANGUAGE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\blanguage-(\w+)\b").unwrap());
|
||||
static HIDELINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\bhidelines=(\S+)").unwrap());
|
||||
|
||||
CODE_BLOCK_RE
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if classes.contains("language-rust") {
|
||||
format!(
|
||||
"<code class=\"{}\">{}</code>",
|
||||
classes,
|
||||
hide_lines_rust(code)
|
||||
)
|
||||
} else {
|
||||
// First try to get the prefix from the code block
|
||||
let hidelines_capture = HIDELINES_REGEX.captures(classes);
|
||||
let hidelines_prefix = match &hidelines_capture {
|
||||
Some(capture) => Some(&capture[1]),
|
||||
None => {
|
||||
// Then look up the prefix by language
|
||||
LANGUAGE_REGEX.captures(classes).and_then(|capture| {
|
||||
code_config.hidelines.get(&capture[1]).map(|p| p.as_str())
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
match hidelines_prefix {
|
||||
Some(prefix) => format!(
|
||||
"<code class=\"{}\">{}</code>",
|
||||
classes,
|
||||
hide_lines_with_prefix(code, prefix)
|
||||
),
|
||||
None => text.to_owned(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn hide_lines_rust(content: &str) -> String {
|
||||
static BORING_LINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap());
|
||||
|
||||
let mut result = String::with_capacity(content.len());
|
||||
for line in content.lines() {
|
||||
let mut lines = content.lines().peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
// Don't include newline on the last line.
|
||||
let newline = if lines.peek().is_none() { "" } else { "\n" };
|
||||
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
|
||||
if &caps[2] == "#" {
|
||||
result += &caps[1];
|
||||
result += &caps[2];
|
||||
result += &caps[3];
|
||||
result += "\n";
|
||||
result += newline;
|
||||
continue;
|
||||
} else if &caps[2] != "!" && &caps[2] != "[" {
|
||||
result += "<span class=\"boring\">";
|
||||
@@ -919,12 +1010,32 @@ fn hide_lines(content: &str) -> String {
|
||||
result += &caps[2];
|
||||
}
|
||||
result += &caps[3];
|
||||
result += "\n";
|
||||
result += newline;
|
||||
result += "</span>";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result += line;
|
||||
result += newline;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn hide_lines_with_prefix(content: &str, prefix: &str) -> String {
|
||||
let mut result = String::with_capacity(content.len());
|
||||
for line in content.lines() {
|
||||
if line.trim_start().starts_with(prefix) {
|
||||
let pos = line.find(prefix).unwrap();
|
||||
let (ws, rest) = (&line[..pos], &line[pos + prefix.len()..]);
|
||||
|
||||
result += "<span class=\"boring\">";
|
||||
result += ws;
|
||||
result += rest;
|
||||
result += "\n";
|
||||
result += "</span>";
|
||||
continue;
|
||||
}
|
||||
result += line;
|
||||
result += "\n";
|
||||
}
|
||||
result
|
||||
@@ -964,7 +1075,10 @@ struct RenderItemContext<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::TextDirection;
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn original_build_header_links() {
|
||||
@@ -993,6 +1107,21 @@ mod tests {
|
||||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1><h3 id="foo-1"><a class="header" href="#foo-1">Foo</a></h3>"##,
|
||||
),
|
||||
// id only
|
||||
(
|
||||
r##"<h1 id="foobar">Foo</h1>"##,
|
||||
r##"<h1 id="foobar"><a class="header" href="#foobar">Foo</a></h1>"##,
|
||||
),
|
||||
// class only
|
||||
(
|
||||
r##"<h1 class="class1 class2">Foo</h1>"##,
|
||||
r##"<h1 id="foo" class="class1 class2"><a class="header" href="#foo">Foo</a></h1>"##,
|
||||
),
|
||||
// both id and class
|
||||
(
|
||||
r##"<h1 id="foobar" class="class1 class2">Foo</h1>"##,
|
||||
r##"<h1 id="foobar" class="class1 class2"><a class="header" href="#foobar">Foo</a></h1>"##,
|
||||
),
|
||||
];
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
@@ -1005,19 +1134,19 @@ mod tests {
|
||||
fn add_playground() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</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>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\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>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n ## bar\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>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\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 ignore\">let s = \"foo\n # bar\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>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
@@ -1035,13 +1164,13 @@ mod tests {
|
||||
fn add_playground_edition2015() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
@@ -1059,13 +1188,13 @@ mod tests {
|
||||
fn add_playground_edition2018() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
@@ -1083,13 +1212,13 @@ mod tests {
|
||||
fn add_playground_edition2021() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
@@ -1103,4 +1232,66 @@ mod tests {
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_lines_language_rust() {
|
||||
let inputs = [
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n# #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>",
|
||||
"<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\">}</span></code></pre>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code></pre>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";</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>\";</code>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = hide_lines(src, &Code::default());
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_lines_language_other() {
|
||||
let inputs = [
|
||||
(
|
||||
"<code class=\"language-python\">~hidden()\nnothidden():\n~ hidden()\n ~hidden()\n nothidden()</code>",
|
||||
"<code class=\"language-python\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",),
|
||||
(
|
||||
"<code class=\"language-python hidelines=!!!\">!!!hidden()\nnothidden():\n!!! hidden()\n !!!hidden()\n nothidden()</code>",
|
||||
"<code class=\"language-python hidelines=!!!\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = hide_lines(
|
||||
src,
|
||||
&Code {
|
||||
hidelines: {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("python".to_string(), "~".to_string());
|
||||
map
|
||||
},
|
||||
},
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_direction() {
|
||||
assert_eq!(json!(TextDirection::RightToLeft), json!("rtl"));
|
||||
assert_eq!(json!(TextDirection::LeftToRight), json!("ltr"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason, Renderable,
|
||||
};
|
||||
|
||||
use crate::utils;
|
||||
use log::{debug, trace};
|
||||
use serde_json::json;
|
||||
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
@@ -24,9 +28,9 @@ impl Target {
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
match *self {
|
||||
Target::Next => {
|
||||
let previous_path = previous_item
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
|
||||
let previous_path = previous_item.get("path").ok_or_else(|| {
|
||||
RenderErrorReason::Other("No path found for chapter in JSON data".to_owned())
|
||||
})?;
|
||||
|
||||
if previous_path == base_path {
|
||||
return Ok(Some(current_item.clone()));
|
||||
@@ -52,15 +56,18 @@ fn find_chapter(
|
||||
debug!("Get data from context");
|
||||
|
||||
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"))
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone()).map_err(|_| {
|
||||
RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
|
||||
})
|
||||
})?;
|
||||
|
||||
let base_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||
})?
|
||||
.replace('\"', "");
|
||||
|
||||
if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
|
||||
@@ -96,7 +103,7 @@ fn find_chapter(
|
||||
}
|
||||
}
|
||||
|
||||
previous = Some(item.clone());
|
||||
previous = Some(item);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
@@ -106,7 +113,7 @@ fn find_chapter(
|
||||
}
|
||||
|
||||
fn render(
|
||||
_h: &Helper<'_, '_>,
|
||||
_h: &Helper<'_>,
|
||||
r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
@@ -120,45 +127,50 @@ fn render(
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||
})?
|
||||
.replace('\"', "");
|
||||
|
||||
context.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&base_path)),
|
||||
json!(utils::fs::path_to_root(base_path)),
|
||||
);
|
||||
|
||||
chapter
|
||||
.get("name")
|
||||
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("No title found for chapter in JSON data".to_owned())
|
||||
})
|
||||
.map(|name| context.insert("title".to_owned(), json!(name)))?;
|
||||
|
||||
chapter
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("No path found for chapter in JSON data".to_owned())
|
||||
})
|
||||
.and_then(|p| {
|
||||
Path::new(p)
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Link could not be converted to str".to_owned())
|
||||
})
|
||||
.map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/"))))
|
||||
})?;
|
||||
|
||||
trace!("Render template");
|
||||
|
||||
_h.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.clone();
|
||||
let local_ctx = Context::wraps(&context)?;
|
||||
t.render(r, &local_ctx, &mut local_rc, out)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
let t = _h
|
||||
.template()
|
||||
.ok_or_else(|| RenderErrorReason::Other("Error with the handlebars template".to_owned()))?;
|
||||
let local_ctx = Context::wraps(&context)?;
|
||||
let mut local_rc = rc.clone();
|
||||
t.render(r, &local_ctx, &mut local_rc, out)
|
||||
}
|
||||
|
||||
pub fn previous(
|
||||
_h: &Helper<'_, '_>,
|
||||
_h: &Helper<'_>,
|
||||
r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
@@ -174,7 +186,7 @@ pub fn previous(
|
||||
}
|
||||
|
||||
pub fn next(
|
||||
_h: &Helper<'_, '_>,
|
||||
_h: &Helper<'_>,
|
||||
r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
};
|
||||
use log::trace;
|
||||
|
||||
pub fn theme_option(
|
||||
h: &Helper<'_, '_>,
|
||||
h: &Helper<'_>,
|
||||
_r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
@@ -10,14 +13,21 @@ pub fn theme_option(
|
||||
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.")
|
||||
RenderErrorReason::ParamTypeMismatchForName(
|
||||
"theme_option",
|
||||
"0".to_owned(),
|
||||
"string".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
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"))?;
|
||||
let default_theme_name = default_theme.as_json().as_str().ok_or_else(|| {
|
||||
RenderErrorReason::ParamTypeMismatchForName(
|
||||
"theme_option",
|
||||
"default_theme".to_owned(),
|
||||
"string".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
out.write(param)?;
|
||||
if param.to_lowercase() == default_theme_name.to_lowercase() {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::path::Path;
|
||||
use std::{cmp::Ordering, collections::BTreeMap};
|
||||
|
||||
use crate::utils;
|
||||
use crate::utils::bracket_escape;
|
||||
use crate::utils::special_escape;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -15,7 +16,7 @@ pub struct RenderToc {
|
||||
impl HelperDef for RenderToc {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
_h: &Helper<'reg, 'rc>,
|
||||
_h: &Helper<'rc>,
|
||||
_r: &'reg Handlebars<'_>,
|
||||
ctx: &'rc Context,
|
||||
rc: &mut RenderContext<'reg, 'rc>,
|
||||
@@ -26,65 +27,48 @@ impl HelperDef for RenderToc {
|
||||
// param is the key of value you want to display
|
||||
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"))
|
||||
.map_err(|_| {
|
||||
RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
|
||||
})
|
||||
})?;
|
||||
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"))?;
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `fold_enable`, bool expected".to_owned())
|
||||
})?;
|
||||
|
||||
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"))?;
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `fold_level`, u64 expected".to_owned())
|
||||
})?;
|
||||
|
||||
// If true, then this is the iframe and we need target="_parent"
|
||||
let is_toc_html = rc
|
||||
.evaluate(ctx, "@root/is_toc_html")?
|
||||
.as_json()
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
|
||||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
// The "index" page, which has this attribute set, is supposed to alias the first chapter in
|
||||
// the book, i.e. the first link. There seems to be no easy way to determine which chapter
|
||||
// the "index" is aliasing from within the renderer, so this is used instead to force the
|
||||
// first link to be active. See further below.
|
||||
let mut is_first_chapter = ctx.data().get("is_index").is_some();
|
||||
|
||||
for item in chapters {
|
||||
// Spacer
|
||||
if item.get("spacer").is_some() {
|
||||
out.write("<li class=\"spacer\"></li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let (section, level) = if let Some(s) = item.get("section") {
|
||||
let (_section, level) = if let Some(s) = item.get("section") {
|
||||
(s.as_str(), s.matches('.').count())
|
||||
} else {
|
||||
("", 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
|
||||
};
|
||||
// Expand if folding is disabled, or if levels that are larger than this would not
|
||||
// be folded.
|
||||
let is_expanded = !fold_enable || level - 1 < (fold_level as usize);
|
||||
|
||||
match level.cmp(¤t_level) {
|
||||
Ordering::Greater => {
|
||||
@@ -104,48 +88,50 @@ impl HelperDef for RenderToc {
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
|
||||
write_li_open_tag(out, is_expanded, !item.contains_key("section"))?;
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
if item.contains_key("spacer") {
|
||||
out.write("<li class=\"spacer\"></li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Part title
|
||||
if let Some(title) = item.get("part") {
|
||||
out.write("<li class=\"part-title\">")?;
|
||||
out.write(&bracket_escape(title))?;
|
||||
out.write(&special_escape(title))?;
|
||||
out.write("</li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Link
|
||||
let path_exists = if let Some(path) =
|
||||
item.get("path")
|
||||
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
||||
{
|
||||
out.write("<a href=\"")?;
|
||||
let path_exists: bool;
|
||||
match item.get("path") {
|
||||
Some(path) if !path.is_empty() => {
|
||||
out.write("<a href=\"")?;
|
||||
let tmp = Path::new(path)
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
.replace('\\', "/");
|
||||
|
||||
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
.replace('\\', "/");
|
||||
|
||||
// Add link
|
||||
out.write(&utils::fs::path_to_root(¤t_path))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
|
||||
if path == ¤t_path || is_first_chapter {
|
||||
is_first_chapter = false;
|
||||
out.write(" class=\"active\"")?;
|
||||
// Add link
|
||||
out.write(&tmp)?;
|
||||
out.write(if is_toc_html {
|
||||
"\" target=\"_parent\">"
|
||||
} else {
|
||||
"\">"
|
||||
})?;
|
||||
path_exists = true;
|
||||
}
|
||||
|
||||
out.write(">")?;
|
||||
true
|
||||
} else {
|
||||
out.write("<div>")?;
|
||||
false
|
||||
};
|
||||
_ => {
|
||||
out.write("<div>")?;
|
||||
path_exists = false;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.no_section_label {
|
||||
// Section does not necessarily exist
|
||||
@@ -157,7 +143,7 @@ impl HelperDef for RenderToc {
|
||||
}
|
||||
|
||||
if let Some(name) = item.get("name") {
|
||||
out.write(&bracket_escape(name))?
|
||||
out.write(&special_escape(name))?
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use elasticlunr::{Index, IndexBuilder};
|
||||
use once_cell::sync::Lazy;
|
||||
use pulldown_cmark::*;
|
||||
|
||||
use crate::book::{Book, BookItem};
|
||||
@@ -10,7 +11,7 @@ use crate::config::Search;
|
||||
use crate::errors::*;
|
||||
use crate::theme::searcher;
|
||||
use crate::utils;
|
||||
|
||||
use log::{debug, warn};
|
||||
use serde::Serialize;
|
||||
|
||||
const MAX_WORD_LENGTH_TO_INDEX: usize = 80;
|
||||
@@ -49,7 +50,7 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
"searchindex.js",
|
||||
format!("Object.assign(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)?;
|
||||
@@ -65,11 +66,24 @@ fn add_doc(
|
||||
index: &mut Index,
|
||||
doc_urls: &mut Vec<String>,
|
||||
anchor_base: &str,
|
||||
section_id: &Option<String>,
|
||||
heading: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
section_id: &Option<CowStr<'_>>,
|
||||
items: &[&str],
|
||||
) {
|
||||
let url = if let Some(ref id) = *section_id {
|
||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||
// Either use the explicit section id the user specified, or generate one
|
||||
// from the heading content.
|
||||
let section_id = section_id.as_ref().map(|id| id.to_string()).or_else(|| {
|
||||
if heading.is_empty() {
|
||||
// In the case where a chapter has no heading, don't set a section id.
|
||||
None
|
||||
} else {
|
||||
Some(utils::unique_id_from_content(heading, id_counter))
|
||||
}
|
||||
});
|
||||
|
||||
let url = if let Some(id) = section_id {
|
||||
Cow::Owned(format!("{anchor_base}#{id}"))
|
||||
} else {
|
||||
Cow::Borrowed(anchor_base)
|
||||
};
|
||||
@@ -118,7 +132,7 @@ fn render_item(
|
||||
let mut id_counter = HashMap::new();
|
||||
while let Some(event) = p.next() {
|
||||
match event {
|
||||
Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
|
||||
Event::Start(Tag::Heading { level, id, .. }) if level as u32 <= max_section_depth => {
|
||||
if !heading.is_empty() {
|
||||
// Section finished, the next heading is following now
|
||||
// Write the data to the index, and clear it for the next section
|
||||
@@ -126,20 +140,21 @@ fn render_item(
|
||||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
&heading,
|
||||
&mut id_counter,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
section_id = None;
|
||||
heading.clear();
|
||||
body.clear();
|
||||
breadcrumbs.pop();
|
||||
}
|
||||
|
||||
section_id = id;
|
||||
in_heading = true;
|
||||
}
|
||||
Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
|
||||
Event::End(TagEnd::Heading(level)) if level as u32 <= max_section_depth => {
|
||||
in_heading = false;
|
||||
section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter));
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||
@@ -156,9 +171,19 @@ fn render_item(
|
||||
html_block.push_str(html);
|
||||
p.next();
|
||||
}
|
||||
|
||||
body.push_str(&clean_html(&html_block));
|
||||
}
|
||||
Event::InlineHtml(html) => {
|
||||
// This is not capable of cleaning inline tags like
|
||||
// `foo <script>…</script>`. The `<script>` tags show up as
|
||||
// individual InlineHtml events, and the content inside is
|
||||
// just a regular Text event. There isn't a very good way to
|
||||
// know how to collect all the content in-between. I'm not
|
||||
// sure if this is easily fixable. It should be extremely
|
||||
// rare, since script and style tags should almost always be
|
||||
// blocks, and worse case you have some noise in the index.
|
||||
body.push_str(&clean_html(&html));
|
||||
}
|
||||
Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => {
|
||||
// Insert spaces where HTML output would usually separate text
|
||||
// to ensure words don't get merged together
|
||||
@@ -178,25 +203,31 @@ fn render_item(
|
||||
Event::FootnoteReference(name) => {
|
||||
let len = footnote_numbers.len() + 1;
|
||||
let number = footnote_numbers.entry(name).or_insert(len);
|
||||
body.push_str(&format!(" [{}] ", number));
|
||||
body.push_str(&format!(" [{number}] "));
|
||||
}
|
||||
Event::TaskListMarker(_checked) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !body.is_empty() || !heading.is_empty() {
|
||||
if heading.is_empty() {
|
||||
let title = if heading.is_empty() {
|
||||
if let Some(chapter) = breadcrumbs.first() {
|
||||
heading = chapter.clone();
|
||||
chapter
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
&heading
|
||||
};
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
&heading,
|
||||
&mut id_counter,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
&[title, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -266,21 +297,19 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
|
||||
}
|
||||
|
||||
fn clean_html(html: &str) -> String {
|
||||
lazy_static! {
|
||||
static ref AMMONIA: ammonia::Builder<'static> = {
|
||||
let mut clean_content = HashSet::new();
|
||||
clean_content.insert("script");
|
||||
clean_content.insert("style");
|
||||
let mut builder = ammonia::Builder::new();
|
||||
builder
|
||||
.tags(HashSet::new())
|
||||
.tag_attributes(HashMap::new())
|
||||
.generic_attributes(HashSet::new())
|
||||
.link_rel(None)
|
||||
.allowed_classes(HashMap::new())
|
||||
.clean_content_tags(clean_content);
|
||||
builder
|
||||
};
|
||||
}
|
||||
static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
|
||||
let mut clean_content = HashSet::new();
|
||||
clean_content.insert("script");
|
||||
clean_content.insert("style");
|
||||
let mut builder = ammonia::Builder::new();
|
||||
builder
|
||||
.tags(HashSet::new())
|
||||
.tag_attributes(HashMap::new())
|
||||
.generic_attributes(HashSet::new())
|
||||
.link_rel(None)
|
||||
.allowed_classes(HashMap::new())
|
||||
.clean_content_tags(clean_content);
|
||||
builder
|
||||
});
|
||||
AMMONIA.clean(html).to_string()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::book::BookItem;
|
||||
use crate::errors::*;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
|
||||
use log::trace;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -37,14 +37,14 @@ impl Renderer for MarkdownRenderer {
|
||||
if !ch.is_draft_chapter() {
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
&ch.path.as_ref().expect("Checked path exists before"),
|
||||
ch.path.as_ref().expect("Checked path exists before"),
|
||||
ch.content.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
fs::create_dir_all(destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -27,6 +27,7 @@ use std::process::{Command, Stdio};
|
||||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use log::{error, info, trace, warn};
|
||||
use toml::Value;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
window.onunload = function () { };
|
||||
|
||||
// Global variable, shared between modules
|
||||
function playground_text(playground) {
|
||||
function playground_text(playground, hidden = true) {
|
||||
let code_block = playground.querySelector("code");
|
||||
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
let editor = window.ace.edit(code_block);
|
||||
return editor.getValue();
|
||||
} else {
|
||||
} else if (hidden) {
|
||||
return code_block.textContent;
|
||||
} else {
|
||||
return code_block.innerText;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +68,7 @@ function playground_text(playground) {
|
||||
}
|
||||
|
||||
// updates the visibility of play button based on `no_run` class and
|
||||
// used crates vs ones available on http://play.rust-lang.org
|
||||
// used crates vs ones available on https://play.rust-lang.org
|
||||
function update_play_button(pre_block, playground_crates) {
|
||||
var play_button = pre_block.querySelector(".play-button");
|
||||
|
||||
@@ -166,7 +168,6 @@ function playground_text(playground) {
|
||||
.filter(function (node) {return node.classList.contains("editable"); })
|
||||
.forEach(function (block) { block.classList.remove('language-rust'); });
|
||||
|
||||
Array
|
||||
code_nodes
|
||||
.filter(function (node) {return !node.classList.contains("editable"); })
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
@@ -178,7 +179,7 @@ function playground_text(playground) {
|
||||
// even if highlighting doesn't apply
|
||||
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
|
||||
|
||||
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
|
||||
Array.from(document.querySelectorAll("code.hljs")).forEach(function (block) {
|
||||
|
||||
var lines = Array.from(block.querySelectorAll('.boring'));
|
||||
// If no lines were hidden, return
|
||||
@@ -224,7 +225,7 @@ function playground_text(playground) {
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
clipButton.className = 'fa fa-copy clip-button';
|
||||
clipButton.className = 'clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
@@ -257,7 +258,7 @@ function playground_text(playground) {
|
||||
|
||||
if (window.playground_copyable) {
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||
copyCodeClipboardButton.className = 'clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
@@ -288,6 +289,10 @@ function playground_text(playground) {
|
||||
var themeToggleButton = document.getElementById('theme-toggle');
|
||||
var themePopup = document.getElementById('theme-list');
|
||||
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
|
||||
var themeIds = [];
|
||||
themePopup.querySelectorAll('button.theme').forEach(function (el) {
|
||||
themeIds.push(el.id);
|
||||
});
|
||||
var stylesheets = {
|
||||
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
|
||||
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
|
||||
@@ -300,6 +305,13 @@ function playground_text(playground) {
|
||||
themePopup.querySelector("button#" + get_theme()).focus();
|
||||
}
|
||||
|
||||
function updateThemeSelected() {
|
||||
themePopup.querySelectorAll('.theme-selected').forEach(function (el) {
|
||||
el.classList.remove('theme-selected');
|
||||
});
|
||||
themePopup.querySelector("button#" + get_theme()).classList.add('theme-selected');
|
||||
}
|
||||
|
||||
function hideThemes() {
|
||||
themePopup.style.display = 'none';
|
||||
themeToggleButton.setAttribute('aria-expanded', false);
|
||||
@@ -309,7 +321,7 @@ function playground_text(playground) {
|
||||
function get_theme() {
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||
if (theme === null || theme === undefined) {
|
||||
if (theme === null || theme === undefined || !themeIds.includes(theme)) {
|
||||
return default_theme;
|
||||
} else {
|
||||
return theme;
|
||||
@@ -338,7 +350,7 @@ function playground_text(playground) {
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor;
|
||||
themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
|
||||
}, 1);
|
||||
|
||||
if (window.ace && window.editors) {
|
||||
@@ -355,6 +367,7 @@ function playground_text(playground) {
|
||||
|
||||
html.classList.remove(previousTheme);
|
||||
html.classList.add(theme);
|
||||
updateThemeSelected();
|
||||
}
|
||||
|
||||
// Set theme
|
||||
@@ -432,7 +445,7 @@ function playground_text(playground) {
|
||||
})();
|
||||
|
||||
(function sidebar() {
|
||||
var html = document.querySelector("html");
|
||||
var body = document.querySelector("body");
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||
@@ -440,8 +453,8 @@ function playground_text(playground) {
|
||||
var firstContact = null;
|
||||
|
||||
function showSidebar() {
|
||||
html.classList.remove('sidebar-hidden')
|
||||
html.classList.add('sidebar-visible');
|
||||
body.classList.remove('sidebar-hidden')
|
||||
body.classList.add('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function (link) {
|
||||
link.setAttribute('tabIndex', 0);
|
||||
});
|
||||
@@ -462,8 +475,8 @@ function playground_text(playground) {
|
||||
});
|
||||
|
||||
function hideSidebar() {
|
||||
html.classList.remove('sidebar-visible')
|
||||
html.classList.add('sidebar-hidden');
|
||||
body.classList.remove('sidebar-visible')
|
||||
body.classList.add('sidebar-hidden');
|
||||
Array.from(sidebarLinks).forEach(function (link) {
|
||||
link.setAttribute('tabIndex', -1);
|
||||
});
|
||||
@@ -474,14 +487,14 @@ function playground_text(playground) {
|
||||
|
||||
// Toggle sidebar
|
||||
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
|
||||
if (html.classList.contains("sidebar-hidden")) {
|
||||
if (body.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")) {
|
||||
} else if (body.classList.contains("sidebar-visible")) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (getComputedStyle(sidebar)['transform'] === 'none') {
|
||||
@@ -497,14 +510,14 @@ function playground_text(playground) {
|
||||
function initResize(e) {
|
||||
window.addEventListener('mousemove', resize, false);
|
||||
window.addEventListener('mouseup', stopResize, false);
|
||||
html.classList.add('sidebar-resizing');
|
||||
body.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
var pos = (e.clientX - sidebar.offsetLeft);
|
||||
if (pos < 20) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (html.classList.contains("sidebar-hidden")) {
|
||||
if (body.classList.contains("sidebar-hidden")) {
|
||||
showSidebar();
|
||||
}
|
||||
pos = Math.min(pos, window.innerWidth - 100);
|
||||
@@ -513,7 +526,7 @@ function playground_text(playground) {
|
||||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize(e) {
|
||||
html.classList.remove('sidebar-resizing');
|
||||
body.classList.remove('sidebar-resizing');
|
||||
window.removeEventListener('mousemove', resize, false);
|
||||
window.removeEventListener('mouseup', stopResize, false);
|
||||
}
|
||||
@@ -542,33 +555,41 @@ function playground_text(playground) {
|
||||
firstContact = null;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Scroll sidebar to current active section
|
||||
var activeSection = document.getElementById("sidebar").querySelector(".active");
|
||||
if (activeSection) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
})();
|
||||
|
||||
(function chapterNavigation() {
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (window.search && window.search.hasFocus()) { return; }
|
||||
var html = document.querySelector('html');
|
||||
|
||||
function next() {
|
||||
var nextButton = document.querySelector('.nav-chapters.next');
|
||||
if (nextButton) {
|
||||
window.location.href = nextButton.href;
|
||||
}
|
||||
}
|
||||
function prev() {
|
||||
var previousButton = document.querySelector('.nav-chapters.previous');
|
||||
if (previousButton) {
|
||||
window.location.href = previousButton.href;
|
||||
}
|
||||
}
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
var nextButton = document.querySelector('.nav-chapters.next');
|
||||
if (nextButton) {
|
||||
window.location.href = nextButton.href;
|
||||
if (html.dir == 'rtl') {
|
||||
prev();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
var previousButton = document.querySelector('.nav-chapters.previous');
|
||||
if (previousButton) {
|
||||
window.location.href = previousButton.href;
|
||||
if (html.dir == 'rtl') {
|
||||
next();
|
||||
} else {
|
||||
prev();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -580,19 +601,19 @@ function playground_text(playground) {
|
||||
|
||||
function hideTooltip(elem) {
|
||||
elem.firstChild.innerText = "";
|
||||
elem.className = 'fa fa-copy clip-button';
|
||||
elem.className = 'clip-button';
|
||||
}
|
||||
|
||||
function showTooltip(elem, msg) {
|
||||
elem.firstChild.innerText = msg;
|
||||
elem.className = 'fa fa-copy tooltipped';
|
||||
elem.className = 'clip-button tooltipped';
|
||||
}
|
||||
|
||||
var clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||
text: function (trigger) {
|
||||
hideTooltip(trigger);
|
||||
let playground = trigger.closest("pre");
|
||||
return playground_text(playground);
|
||||
return playground_text(playground, false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -667,13 +688,14 @@ function playground_text(playground) {
|
||||
}, { passive: true });
|
||||
})();
|
||||
(function controllBorder() {
|
||||
menu.classList.remove('bordered');
|
||||
document.addEventListener('scroll', function () {
|
||||
function updateBorder() {
|
||||
if (menu.offsetTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
} else {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
updateBorder();
|
||||
document.addEventListener('scroll', updateBorder, { passive: true });
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
/* CSS for UI elements (a.k.a. chrome) */
|
||||
|
||||
@import 'variables.css';
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background: var(--bg);
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
html {
|
||||
scrollbar-color: var(--scrollbar) var(--bg);
|
||||
}
|
||||
@@ -18,6 +10,19 @@ a > .hljs {
|
||||
color: var(--links);
|
||||
}
|
||||
|
||||
/*
|
||||
body-container is necessary because mobile browsers don't seem to like
|
||||
overflow-x on the body tag when there is a <meta name="viewport"> tag.
|
||||
*/
|
||||
#body-container {
|
||||
/*
|
||||
This is used when the sidebar pushes the body content off the side of
|
||||
the screen on small screens. Without it, dragging on mobile Safari
|
||||
will want to reposition the viewport in a weird way.
|
||||
*/
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
/* Menu Bar */
|
||||
|
||||
#menu-bar,
|
||||
@@ -30,14 +35,14 @@ a > .hljs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: var(--bg);
|
||||
border-bottom-color: var(--bg);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-block-end-color: var(--bg);
|
||||
border-block-end-width: 1px;
|
||||
border-block-end-style: solid;
|
||||
}
|
||||
#menu-bar.sticky,
|
||||
.js #menu-bar-hover-placeholder:hover + #menu-bar,
|
||||
.js #menu-bar:hover,
|
||||
.js.sidebar-visible #menu-bar {
|
||||
#menu-bar-hover-placeholder:hover + #menu-bar,
|
||||
#menu-bar:hover,
|
||||
html.sidebar-visible #menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0 !important;
|
||||
@@ -49,7 +54,7 @@ a > .hljs {
|
||||
height: var(--menu-bar-height);
|
||||
}
|
||||
#menu-bar.bordered {
|
||||
border-bottom-color: var(--table-border-color);
|
||||
border-block-end-color: var(--table-border-color);
|
||||
}
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
position: relative;
|
||||
@@ -86,7 +91,7 @@ a > .hljs {
|
||||
display: flex;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.no-js .left-buttons {
|
||||
html:not(.js) .left-buttons button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -102,7 +107,7 @@ a > .hljs {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.js .menu-title {
|
||||
.menu-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -153,7 +158,7 @@ a > .hljs {
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
margin-top: 50px;
|
||||
margin-block-start: 50px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -166,23 +171,34 @@ a > .hljs {
|
||||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
.previous {
|
||||
float: left;
|
||||
}
|
||||
/* Only Firefox supports flow-relative values */
|
||||
.previous { float: left; }
|
||||
[dir=rtl] .previous { float: right; }
|
||||
|
||||
/* Only Firefox supports flow-relative values */
|
||||
.next {
|
||||
float: right;
|
||||
right: var(--page-padding);
|
||||
}
|
||||
[dir=rtl] .next {
|
||||
float: left;
|
||||
right: unset;
|
||||
left: var(--page-padding);
|
||||
}
|
||||
|
||||
/* Use the correct buttons for RTL layouts*/
|
||||
[dir=rtl] .previous i.fa-angle-left:before {content:"\f105";}
|
||||
[dir=rtl] .next i.fa-angle-right:before { content:"\f104"; }
|
||||
|
||||
@media only screen and (max-width: 1080px) {
|
||||
.nav-wide-wrapper { display: none; }
|
||||
.nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* sidebar-visible */
|
||||
@media only screen and (max-width: 1380px) {
|
||||
.sidebar-visible .nav-wide-wrapper { display: none; }
|
||||
.sidebar-visible .nav-wrapper { display: block; }
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; }
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
@@ -229,13 +245,13 @@ pre > .buttons :hover {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
pre > .buttons button {
|
||||
cursor: inherit;
|
||||
margin: 0px 5px;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
padding: 4px 4px 3px 5px;
|
||||
font-size: 23px;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
@@ -246,13 +262,40 @@ pre > .buttons button {
|
||||
transition-property: color,border-color,background-color;
|
||||
color: var(--icons);
|
||||
}
|
||||
|
||||
pre > .buttons button.clip-button {
|
||||
padding: 2px 4px 0px 6px;
|
||||
}
|
||||
pre > .buttons button.clip-button::before {
|
||||
/* clipboard image from octicons (https://github.com/primer/octicons/tree/v2.0.0) MIT license
|
||||
*/
|
||||
content: url('data:image/svg+xml,<svg width="21" height="20" viewBox="0 0 24 25" \
|
||||
xmlns="http://www.w3.org/2000/svg" aria-label="Copy to clipboard">\
|
||||
<path d="M18 20h2v3c0 1-1 2-2 2H2c-.998 0-2-1-2-2V5c0-.911.755-1.667 1.667-1.667h5A3.323 3.323 0 \
|
||||
0110 0a3.323 3.323 0 013.333 3.333h5C19.245 3.333 20 4.09 20 5v8.333h-2V9H2v14h16v-3zM3 \
|
||||
7h14c0-.911-.793-1.667-1.75-1.667H13.5c-.957 0-1.75-.755-1.75-1.666C11.75 2.755 10.957 2 10 \
|
||||
2s-1.75.755-1.75 1.667c0 .911-.793 1.666-1.75 1.666H4.75C3.793 5.333 3 6.09 3 7z"/>\
|
||||
<path d="M4 19h6v2H4zM12 11H4v2h8zM4 17h4v-2H4zM15 15v-3l-4.5 4.5L15 21v-3l8.027-.032L23 15z"/>\
|
||||
</svg>');
|
||||
filter: var(--copy-button-filter);
|
||||
}
|
||||
pre > .buttons button.clip-button:hover::before {
|
||||
filter: var(--copy-button-filter-hover);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
pre > .buttons button {
|
||||
/* On mobile, make it easier to tap buttons. */
|
||||
padding: 0.3rem 1rem;
|
||||
}
|
||||
|
||||
.sidebar-resize-indicator {
|
||||
/* Hide resize indicator on devices with limited accuracy */
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
pre > code {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@@ -266,7 +309,7 @@ pre > code {
|
||||
}
|
||||
|
||||
pre > .result {
|
||||
margin-top: 10px;
|
||||
margin-block-start: 10px;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
@@ -277,8 +320,14 @@ pre > .result {
|
||||
|
||||
mark {
|
||||
border-radius: 2px;
|
||||
padding: 0 3px 1px 3px;
|
||||
margin: 0 -3px -1px -3px;
|
||||
padding-block-start: 0;
|
||||
padding-block-end: 1px;
|
||||
padding-inline-start: 3px;
|
||||
padding-inline-end: 3px;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: -1px;
|
||||
margin-inline-start: -3px;
|
||||
margin-inline-end: -3px;
|
||||
background-color: var(--search-mark-bg);
|
||||
transition: background-color 300ms linear;
|
||||
cursor: pointer;
|
||||
@@ -290,14 +339,17 @@ mark.fade-out {
|
||||
}
|
||||
|
||||
.searchbar-outer {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
|
||||
#searchbar {
|
||||
width: 100%;
|
||||
margin: 5px auto 0px auto;
|
||||
margin-block-start: 5px;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
padding: 10px 16px;
|
||||
transition: box-shadow 300ms ease-in-out;
|
||||
border: 1px solid var(--searchbar-border-color);
|
||||
@@ -313,20 +365,23 @@ mark.fade-out {
|
||||
.searchresults-header {
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
padding: 18px 0 0 5px;
|
||||
padding-block-start: 18px;
|
||||
padding-block-end: 0;
|
||||
padding-inline-start: 5px;
|
||||
padding-inline-end: 0;
|
||||
color: var(--searchresults-header-fg);
|
||||
}
|
||||
|
||||
.searchresults-outer {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
border-bottom: 1px dashed var(--searchresults-border-color);
|
||||
border-block-end: 1px dashed var(--searchresults-border-color);
|
||||
}
|
||||
|
||||
ul#searchresults {
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
ul#searchresults li {
|
||||
margin: 10px 0px;
|
||||
@@ -339,7 +394,10 @@ ul#searchresults li.focus {
|
||||
ul#searchresults span.teaser {
|
||||
display: block;
|
||||
clear: both;
|
||||
margin: 5px 0 0 20px;
|
||||
margin-block-start: 5px;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: 20px;
|
||||
margin-inline-end: 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
ul#searchresults span.teaser em {
|
||||
@@ -362,13 +420,30 @@ ul#searchresults span.teaser em {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.sidebar-iframe-inner {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
padding: 10px 10px;
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.sidebar-iframe-outer {
|
||||
border: none;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
[dir=rtl] .sidebar { left: unset; right: 0; }
|
||||
.sidebar-resizing {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.js:not(.sidebar-resizing) .sidebar {
|
||||
html:not(.sidebar-resizing) .sidebar {
|
||||
transition: transform 0.3s; /* Animation: slide away */
|
||||
}
|
||||
.sidebar code {
|
||||
@@ -387,16 +462,35 @@ ul#searchresults span.teaser em {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
width: 0;
|
||||
right: 0;
|
||||
right: calc(var(--sidebar-resize-indicator-width) * -1);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-resize-handle .sidebar-resize-indicator {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background-color: var(--icons);
|
||||
margin-inline-start: var(--sidebar-resize-indicator-space);
|
||||
}
|
||||
|
||||
[dir=rtl] .sidebar .sidebar-resize-handle {
|
||||
left: calc(var(--sidebar-resize-indicator-width) * -1);
|
||||
right: unset;
|
||||
}
|
||||
.js .sidebar .sidebar-resize-handle {
|
||||
cursor: col-resize;
|
||||
width: 5px;
|
||||
width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space));
|
||||
}
|
||||
.sidebar-hidden .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width)));
|
||||
/* sidebar-hidden */
|
||||
#sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
z-index: -1;
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
.sidebar::-webkit-scrollbar {
|
||||
background: var(--sidebar-bg);
|
||||
@@ -405,19 +499,26 @@ ul#searchresults span.teaser em {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
|
||||
.sidebar-visible .page-wrapper {
|
||||
transform: translateX(var(--sidebar-width));
|
||||
/* sidebar-visible */
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
@media only screen and (min-width: 620px) {
|
||||
.sidebar-visible .page-wrapper {
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: none;
|
||||
margin-inline-start: calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width));
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: none;
|
||||
margin-left: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
.chapter {
|
||||
list-style: none outside none;
|
||||
padding-left: 0;
|
||||
padding-inline-start: 0;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
|
||||
@@ -447,7 +548,7 @@ ul#searchresults span.teaser em {
|
||||
.chapter li > a.toggle {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-inline-start: auto;
|
||||
padding: 0 10px;
|
||||
user-select: none;
|
||||
opacity: 0.68;
|
||||
@@ -464,7 +565,7 @@ ul#searchresults span.teaser em {
|
||||
|
||||
.chapter li.chapter-item {
|
||||
line-height: 1.5em;
|
||||
margin-top: 0.6em;
|
||||
margin-block-start: 0.6em;
|
||||
}
|
||||
|
||||
.chapter li.expanded > a.toggle div {
|
||||
@@ -487,7 +588,7 @@ ul#searchresults span.teaser em {
|
||||
|
||||
.section {
|
||||
list-style: none outside none;
|
||||
padding-left: 20px;
|
||||
padding-inline-start: 20px;
|
||||
line-height: 1.9em;
|
||||
}
|
||||
|
||||
@@ -507,7 +608,10 @@ ul#searchresults span.teaser em {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: none;
|
||||
/* Don't let the children's background extend past the rounded corners. */
|
||||
overflow: hidden;
|
||||
}
|
||||
[dir=rtl] .theme-popup { left: unset; right: 10px; }
|
||||
.theme-popup .default {
|
||||
color: var(--icons);
|
||||
}
|
||||
@@ -515,10 +619,10 @@ ul#searchresults span.teaser em {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 2px 10px;
|
||||
padding: 2px 20px;
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
@@ -527,8 +631,10 @@ ul#searchresults span.teaser em {
|
||||
.theme-popup .theme:hover {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
.theme-popup .theme:hover:first-child,
|
||||
.theme-popup .theme:hover:last-child {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
|
||||
.theme-selected::before {
|
||||
display: inline-block;
|
||||
content: "✓";
|
||||
margin-inline-start: -14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/* Base styles and content styles */
|
||||
|
||||
@import 'variables.css';
|
||||
|
||||
:root {
|
||||
/* Browser default font-size is 16px, this way 1 rem = 10px */
|
||||
font-size: 62.5%;
|
||||
color-scheme: var(--color-scheme);
|
||||
}
|
||||
|
||||
html {
|
||||
@@ -22,8 +21,9 @@ body {
|
||||
}
|
||||
|
||||
code {
|
||||
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 */
|
||||
font-family: var(--mono-font) !important;
|
||||
font-size: var(--code-font-size);
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
/* make long words/inline code not x overflow */
|
||||
@@ -47,13 +47,13 @@ h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||
.hide-boring .boring { display: none; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
h2, h3 { margin-top: 2.5em; }
|
||||
h4, h5 { margin-top: 2em; }
|
||||
h2, h3 { margin-block-start: 2.5em; }
|
||||
h4, h5 { margin-block-start: 2em; }
|
||||
|
||||
.header + .header h3,
|
||||
.header + .header h4,
|
||||
.header + .header h5 {
|
||||
margin-top: 1em;
|
||||
margin-block-start: 1em;
|
||||
}
|
||||
|
||||
h1:target::before,
|
||||
@@ -64,7 +64,7 @@ h5:target::before,
|
||||
h6:target::before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-left: -30px;
|
||||
margin-inline-start: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
@@ -73,28 +73,34 @@ h6:target::before {
|
||||
https://bugs.webkit.org/show_bug.cgi?id=218076
|
||||
*/
|
||||
:target {
|
||||
/* Safari does not support logical properties */
|
||||
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 */
|
||||
margin-block-start: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
|
||||
}
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
.no-js .page-wrapper,
|
||||
.js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
[dir=rtl] .js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
padding: 0 5px 50px 5px;
|
||||
}
|
||||
.content main {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
.content p { line-height: 1.45em; }
|
||||
@@ -144,14 +150,59 @@ blockquote {
|
||||
padding: 0 20px;
|
||||
color: var(--fg);
|
||||
background-color: var(--quote-bg);
|
||||
border-top: .1em solid var(--quote-border);
|
||||
border-bottom: .1em solid var(--quote-border);
|
||||
border-block-start: .1em solid var(--quote-border);
|
||||
border-block-end: .1em solid var(--quote-border);
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin: 20px;
|
||||
padding: 0 20px;
|
||||
border-inline-start: 2px solid var(--warning-border);
|
||||
}
|
||||
|
||||
.warning:before {
|
||||
position: absolute;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-inline-start: calc(-1.5rem - 21px);
|
||||
content: "ⓘ";
|
||||
text-align: center;
|
||||
background-color: var(--bg);
|
||||
color: var(--warning-border);
|
||||
font-weight: bold;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
blockquote .warning:before {
|
||||
background-color: var(--quote-bg);
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: var(--table-border-color);
|
||||
border-radius: 4px;
|
||||
border: solid 1px var(--theme-popup-border);
|
||||
box-shadow: inset 0 -1px 0 var(--theme-hover);
|
||||
display: inline-block;
|
||||
font-size: var(--code-font-size);
|
||||
font-family: var(--mono-font);
|
||||
line-height: 10px;
|
||||
padding: 4px 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
sup {
|
||||
/* Set the line-height for superscript and footnote references so that there
|
||||
isn't an awkward space appearing above lines that contain the footnote.
|
||||
|
||||
See https://github.com/rust-lang/mdBook/pull/2443#discussion_r1813773583
|
||||
for an explanation.
|
||||
*/
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
:not(.footnote-definition) + .footnote-definition,
|
||||
.footnote-definition + :not(.footnote-definition) {
|
||||
margin-top: 2em;
|
||||
margin-block-start: 2em;
|
||||
}
|
||||
.footnote-definition {
|
||||
font-size: 0.9em;
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
}
|
||||
|
||||
#page-wrapper.page-wrapper {
|
||||
transform: none;
|
||||
margin-left: 0px;
|
||||
transform: none !important;
|
||||
margin-inline-start: 0px;
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
@@ -23,11 +23,7 @@
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #666666;
|
||||
border-radius: 5px;
|
||||
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact;
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
pre > .buttons {
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
|
||||
:root {
|
||||
--sidebar-width: 300px;
|
||||
--sidebar-resize-indicator-width: 8px;
|
||||
--sidebar-resize-indicator-space: 2px;
|
||||
--page-padding: 15px;
|
||||
--content-max-width: 750px;
|
||||
--menu-bar-height: 50px;
|
||||
--mono-font: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
--code-font-size: 0.875em /* please adjust the ace font size accordingly in editor.js */
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
@@ -36,6 +40,8 @@
|
||||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(210, 25%, 13%);
|
||||
--table-header-bg: hsl(210, 25%, 28%);
|
||||
--table-alternate-bg: hsl(210, 25%, 11%);
|
||||
@@ -48,6 +54,13 @@
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #252932;
|
||||
--search-mark-bg: #e3b171;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(45%) sepia(6%) saturate(621%) hue-rotate(198deg) brightness(99%) contrast(85%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(68%) sepia(55%) saturate(531%) hue-rotate(341deg) brightness(104%) contrast(101%);
|
||||
}
|
||||
|
||||
.coal {
|
||||
@@ -76,6 +89,8 @@
|
||||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
@@ -88,9 +103,16 @@
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%);
|
||||
}
|
||||
|
||||
.light {
|
||||
.light, html:not(.js) {
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--fg: hsl(0, 0%, 0%);
|
||||
|
||||
@@ -116,6 +138,8 @@
|
||||
--quote-bg: hsl(197, 37%, 96%);
|
||||
--quote-border: hsl(197, 37%, 91%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(0, 0%, 95%);
|
||||
--table-header-bg: hsl(0, 0%, 80%);
|
||||
--table-alternate-bg: hsl(0, 0%, 97%);
|
||||
@@ -128,6 +152,13 @@
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #e4f2fe;
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: light;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(45.49%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(14%) sepia(93%) saturate(4250%) hue-rotate(243deg) brightness(99%) contrast(130%);
|
||||
}
|
||||
|
||||
.navy {
|
||||
@@ -156,6 +187,8 @@
|
||||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(226, 23%, 16%);
|
||||
--table-header-bg: hsl(226, 23%, 31%);
|
||||
--table-alternate-bg: hsl(226, 23%, 14%);
|
||||
@@ -168,6 +201,13 @@
|
||||
--searchresults-border-color: #5c5c68;
|
||||
--searchresults-li-bg: #242430;
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(46%) sepia(20%) saturate(1537%) hue-rotate(156deg) brightness(85%) contrast(90%);
|
||||
}
|
||||
|
||||
.rust {
|
||||
@@ -196,6 +236,8 @@
|
||||
--quote-bg: hsl(60, 5%, 75%);
|
||||
--quote-border: hsl(60, 5%, 70%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(60, 9%, 82%);
|
||||
--table-header-bg: #b3a497;
|
||||
--table-alternate-bg: hsl(60, 9%, 84%);
|
||||
@@ -208,10 +250,15 @@
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #dec2a2;
|
||||
--search-mark-bg: #e69f67;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(77%) sepia(16%) saturate(1798%) hue-rotate(328deg) brightness(98%) contrast(83%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.light.no-js {
|
||||
html:not(.js) {
|
||||
--bg: hsl(200, 7%, 8%);
|
||||
--fg: #98a3ad;
|
||||
|
||||
@@ -237,6 +284,8 @@
|
||||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
@@ -249,5 +298,12 @@
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-attr,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,11 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}">
|
||||
<html lang="{{ language }}" class="{{ default_theme }} sidebar-visible" dir="{{ text_direction }}">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
{{#if is_print }}
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="robots" content="noindex">
|
||||
{{/if}}
|
||||
{{#if base_url}}
|
||||
<base href="{{ base_url }}">
|
||||
@@ -15,10 +15,9 @@
|
||||
<!-- 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" />
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
{{#if favicon_svg}}
|
||||
<link rel="icon" href="{{ path_to_root }}favicon.svg">
|
||||
@@ -51,18 +50,19 @@
|
||||
|
||||
{{#if mathjax_support}}
|
||||
<!-- MathJax -->
|
||||
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
</head>
|
||||
<body>
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
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">
|
||||
<script>
|
||||
try {
|
||||
var theme = localStorage.getItem('mdbook-theme');
|
||||
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||
@@ -78,55 +78,65 @@
|
||||
</script>
|
||||
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var theme;
|
||||
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')
|
||||
const html = document.documentElement;
|
||||
html.classList.remove('{{ default_theme }}')
|
||||
html.classList.add(theme);
|
||||
html.classList.add('js');
|
||||
html.classList.add("js");
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script type="text/javascript">
|
||||
var html = document.querySelector('html');
|
||||
var sidebar = 'hidden';
|
||||
<script>
|
||||
var sidebar = null;
|
||||
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||
sidebar = sidebar || 'visible';
|
||||
} else {
|
||||
sidebar = 'hidden';
|
||||
}
|
||||
sidebar_toggle.checked = sidebar === 'visible';
|
||||
html.classList.remove('sidebar-visible');
|
||||
html.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
{{#toc}}{{/toc}}
|
||||
<!-- populated by js -->
|
||||
<div class="sidebar-scrollbox"></div>
|
||||
<noscript>
|
||||
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
|
||||
</noscript>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||
</nav>
|
||||
|
||||
<script async src="{{ path_to_root }}toc.js"></script>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
{{> header}}
|
||||
<div id="menu-bar-hover-placeholder"></div>
|
||||
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||
<div id="menu-bar" class="menu-bar sticky">
|
||||
<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">
|
||||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
</label>
|
||||
<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>
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">Light</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">
|
||||
@@ -171,7 +181,7 @@
|
||||
{{/if}}
|
||||
|
||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||
@@ -193,7 +203,7 @@
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
@@ -211,7 +221,7 @@
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a rel="next" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<a rel="next prefetch" 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}}
|
||||
@@ -221,7 +231,7 @@
|
||||
|
||||
{{#if live_reload_endpoint}}
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
|
||||
const socket = new WebSocket(wsAddress);
|
||||
@@ -240,7 +250,7 @@
|
||||
|
||||
{{#if google_analytics}}
|
||||
<!-- Google Analytics Tag -->
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var localAddrs = ["localhost", "127.0.0.1", ""];
|
||||
|
||||
// make sure we don't activate google analytics if the developer is
|
||||
@@ -258,43 +268,43 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_line_numbers}}
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
window.playground_line_numbers = true;
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_copyable}}
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
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>
|
||||
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}ace.js"></script>
|
||||
<script src="{{ path_to_root }}editor.js"></script>
|
||||
<script src="{{ path_to_root }}mode-rust.js"></script>
|
||||
<script src="{{ path_to_root }}theme-dawn.js"></script>
|
||||
<script src="{{ path_to_root }}theme-tomorrow_night.js"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if search_js}}
|
||||
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}elasticlunr.min.js"></script>
|
||||
<script src="{{ path_to_root }}mark.min.js"></script>
|
||||
<script src="{{ path_to_root }}searcher.js"></script>
|
||||
{{/if}}
|
||||
|
||||
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}clipboard.min.js"></script>
|
||||
<script src="{{ path_to_root }}highlight.js"></script>
|
||||
<script src="{{ path_to_root }}book.js"></script>
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
|
||||
<script src="{{ ../path_to_root }}{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
{{#if mathjax_support}}
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
MathJax.Hub.Register.StartupHook('End', function() {
|
||||
window.setTimeout(window.print, 100);
|
||||
@@ -302,7 +312,7 @@
|
||||
});
|
||||
</script>
|
||||
{{else}}
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
window.setTimeout(window.print, 100);
|
||||
});
|
||||
@@ -310,5 +320,6 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,14 +9,16 @@ pub mod searcher;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
use log::warn;
|
||||
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 TOC_JS: &[u8] = include_bytes!("toc.js.hbs");
|
||||
pub static TOC_HTML: &[u8] = include_bytes!("toc.html.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");
|
||||
@@ -50,10 +52,14 @@ pub struct Theme {
|
||||
pub head: Vec<u8>,
|
||||
pub redirect: Vec<u8>,
|
||||
pub header: Vec<u8>,
|
||||
pub toc_js: Vec<u8>,
|
||||
pub toc_html: Vec<u8>,
|
||||
pub chrome_css: Vec<u8>,
|
||||
pub general_css: Vec<u8>,
|
||||
pub print_css: Vec<u8>,
|
||||
pub variables_css: Vec<u8>,
|
||||
pub fonts_css: Option<Vec<u8>>,
|
||||
pub font_files: Vec<PathBuf>,
|
||||
pub favicon_png: Option<Vec<u8>>,
|
||||
pub favicon_svg: Option<Vec<u8>>,
|
||||
pub js: Vec<u8>,
|
||||
@@ -83,6 +89,8 @@ impl Theme {
|
||||
(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("toc.js.hbs"), &mut theme.toc_js),
|
||||
(theme_dir.join("toc.html.hbs"), &mut theme.toc_html),
|
||||
(theme_dir.join("book.js"), &mut theme.js),
|
||||
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
|
||||
(theme_dir.join("css/general.css"), &mut theme.general_css),
|
||||
@@ -104,7 +112,7 @@ impl Theme {
|
||||
),
|
||||
];
|
||||
|
||||
let load_with_warn = |filename: &Path, dest| {
|
||||
let load_with_warn = |filename: &Path, dest: &mut Vec<u8>| {
|
||||
if !filename.exists() {
|
||||
// Don't warn if the file doesn't exist.
|
||||
return false;
|
||||
@@ -121,6 +129,29 @@ impl Theme {
|
||||
load_with_warn(&filename, dest);
|
||||
}
|
||||
|
||||
let fonts_dir = theme_dir.join("fonts");
|
||||
if fonts_dir.exists() {
|
||||
let mut fonts_css = Vec::new();
|
||||
if load_with_warn(&fonts_dir.join("fonts.css"), &mut fonts_css) {
|
||||
theme.fonts_css.replace(fonts_css);
|
||||
}
|
||||
if let Ok(entries) = fonts_dir.read_dir() {
|
||||
theme.font_files = entries
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
if entry.file_name() == "fonts.css" {
|
||||
None
|
||||
} else if entry.file_type().ok()?.is_dir() {
|
||||
log::info!("skipping font directory {:?}", entry.path());
|
||||
None
|
||||
} else {
|
||||
Some(entry.path())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -149,10 +180,14 @@ impl Default for Theme {
|
||||
head: HEAD.to_owned(),
|
||||
redirect: REDIRECT.to_owned(),
|
||||
header: HEADER.to_owned(),
|
||||
toc_js: TOC_JS.to_owned(),
|
||||
toc_html: TOC_HTML.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(),
|
||||
fonts_css: None,
|
||||
font_files: Vec::new(),
|
||||
favicon_png: Some(FAVICON_PNG.to_owned()),
|
||||
favicon_svg: Some(FAVICON_SVG.to_owned()),
|
||||
js: JS.to_owned(),
|
||||
@@ -185,7 +220,6 @@ fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
|
||||
#[test]
|
||||
@@ -206,13 +240,15 @@ mod tests {
|
||||
"head.hbs",
|
||||
"redirect.hbs",
|
||||
"header.hbs",
|
||||
"toc.js.hbs",
|
||||
"toc.html.hbs",
|
||||
"favicon.png",
|
||||
"favicon.svg",
|
||||
"css/chrome.css",
|
||||
"css/fonts.css",
|
||||
"css/general.css",
|
||||
"css/print.css",
|
||||
"css/variables.css",
|
||||
"fonts/fonts.css",
|
||||
"book.js",
|
||||
"highlight.js",
|
||||
"tomorrow-night.css",
|
||||
@@ -223,6 +259,7 @@ mod tests {
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
|
||||
fs::create_dir(temp.path().join("css")).unwrap();
|
||||
fs::create_dir(temp.path().join("fonts")).unwrap();
|
||||
|
||||
// "touch" all of the special files so we have empty copies
|
||||
for file in &files {
|
||||
@@ -236,10 +273,14 @@ mod tests {
|
||||
head: Vec::new(),
|
||||
redirect: Vec::new(),
|
||||
header: Vec::new(),
|
||||
toc_js: Vec::new(),
|
||||
toc_html: Vec::new(),
|
||||
chrome_css: Vec::new(),
|
||||
general_css: Vec::new(),
|
||||
print_css: Vec::new(),
|
||||
variables_css: Vec::new(),
|
||||
fonts_css: Some(Vec::new()),
|
||||
font_files: Vec::new(),
|
||||
favicon_png: Some(Vec::new()),
|
||||
favicon_svg: Some(Vec::new()),
|
||||
js: Vec::new(),
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0;URL='{{url}}'">
|
||||
<meta rel="canonical" href="{{url}}">
|
||||
<meta http-equiv="refresh" content="0; URL={{url}}">
|
||||
<link rel="canonical" href="{{url}}">
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
|
||||
|
||||
@@ -316,7 +316,7 @@ window.search = window.search || {};
|
||||
|
||||
// Eventhandler for keyevents on `document`
|
||||
function globalKeyHandler(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; }
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)) { return; }
|
||||
|
||||
if (e.keyCode === ESCAPE_KEYCODE) {
|
||||
e.preventDefault();
|
||||
|
||||
43
src/theme/toc.html.hbs
Normal file
43
src/theme/toc.html.hbs
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
|
||||
<head>
|
||||
<!-- sidebar iframe generated using mdBook
|
||||
|
||||
This is a frame, and not included directly in the page, to control the total size of the
|
||||
book. The TOC contains an entry for each page, so if each page includes a copy of the TOC,
|
||||
the total size of the page becomes O(n**2).
|
||||
|
||||
The frame is only used as a fallback when JS is turned off. When it's on, the sidebar is
|
||||
instead added to the main page by `toc.js` instead. The JavaScript mode is better
|
||||
because, when running in a `file:///` URL, the iframed page would not be Same-Origin as
|
||||
the rest of the page, so the sidebar and the main page theme would fall out of sync.
|
||||
-->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="robots" content="noindex">
|
||||
{{#if base_url}}
|
||||
<base href="{{ base_url }}">
|
||||
{{/if}}
|
||||
<!-- Custom HTML head -->
|
||||
{{> head}}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<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">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
{{/if}}
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
|
||||
{{/if}}
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
{{/each}}
|
||||
</head>
|
||||
<body class="sidebar-iframe-inner">
|
||||
{{#toc}}{{/toc}}
|
||||
</body>
|
||||
</html>
|
||||
54
src/theme/toc.js.hbs
Normal file
54
src/theme/toc.js.hbs
Normal file
@@ -0,0 +1,54 @@
|
||||
// Populate the sidebar
|
||||
//
|
||||
// This is a script, and not included directly in the page, to control the total size of the book.
|
||||
// The TOC contains an entry for each page, so if each page includes a copy of the TOC,
|
||||
// the total size of the page becomes O(n**2).
|
||||
var sidebarScrollbox = document.querySelector("#sidebar .sidebar-scrollbox");
|
||||
sidebarScrollbox.innerHTML = '{{#toc}}{{/toc}}';
|
||||
(function() {
|
||||
let current_page = document.location.href.toString();
|
||||
if (current_page.endsWith("/")) {
|
||||
current_page += "index.html";
|
||||
}
|
||||
var links = sidebarScrollbox.querySelectorAll("a");
|
||||
var l = links.length;
|
||||
for (var i = 0; i < l; ++i) {
|
||||
var link = links[i];
|
||||
var href = link.getAttribute("href");
|
||||
if (href && !href.startsWith("#") && !/^(?:[a-z+]+:)?\/\//.test(href)) {
|
||||
link.href = path_to_root + href;
|
||||
}
|
||||
// The "index" page is supposed to alias the first chapter in the book.
|
||||
if (link.href === current_page || (i === 0 && path_to_root === "" && current_page.endsWith("/index.html"))) {
|
||||
link.classList.add("active");
|
||||
var parent = link.parentElement;
|
||||
while (parent) {
|
||||
if (parent.tagName === "LI" && parent.previousElementSibling) {
|
||||
if (parent.previousElementSibling.classList.contains("chapter-item")) {
|
||||
parent.previousElementSibling.classList.add("expanded");
|
||||
}
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Track and set sidebar scroll position
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Tomorrow Night Theme */
|
||||
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||
/* https://github.com/jmblog/color-themes-for-highlightjs */
|
||||
/* Original theme - https://github.com/chriskempson/tomorrow-theme */
|
||||
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||
/* https://github.com/jmblog/color-themes-for-highlightjs */
|
||||
|
||||
/* Tomorrow Comment */
|
||||
.hljs-comment {
|
||||
@@ -11,6 +11,7 @@
|
||||
/* Tomorrow Red */
|
||||
.hljs-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-attr,
|
||||
.hljs-tag,
|
||||
.hljs-regexp,
|
||||
.ruby .hljs-constant,
|
||||
@@ -54,6 +55,7 @@
|
||||
|
||||
/* Tomorrow Aqua */
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.css .hljs-hexcolor {
|
||||
color: #8abeb7;
|
||||
}
|
||||
|
||||
189
src/utils/fs.rs
189
src/utils/fs.rs
@@ -1,5 +1,5 @@
|
||||
use crate::errors::*;
|
||||
use std::convert::Into;
|
||||
use log::{debug, trace};
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
@@ -37,7 +37,6 @@ pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8])
|
||||
/// Consider [submitting a new issue](https://github.com/rust-lang/mdBook/issues)
|
||||
/// or a [pull-request](https://github.com/rust-lang/mdBook/pulls) to improve it.
|
||||
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
debug!("path_to_root");
|
||||
// Remove filename and add "../" for every directory
|
||||
|
||||
path.into()
|
||||
@@ -73,14 +72,12 @@ pub fn create_file(path: &Path) -> Result<File> {
|
||||
|
||||
/// Removes all the content of a directory but not the directory itself
|
||||
pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||
for item in fs::read_dir(dir)? {
|
||||
if let Ok(item) = item {
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
fs::remove_dir_all(item)?;
|
||||
} else {
|
||||
fs::remove_file(item)?;
|
||||
}
|
||||
for item in fs::read_dir(dir)?.flatten() {
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
fs::remove_dir_all(item)?;
|
||||
} else {
|
||||
fs::remove_file(item)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -109,77 +106,102 @@ pub fn copy_files_except_ext(
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(from)? {
|
||||
let entry = entry?;
|
||||
let entry = entry?.path();
|
||||
let metadata = entry
|
||||
.path()
|
||||
.metadata()
|
||||
.with_context(|| format!("Failed to read {:?}", entry.path()))?;
|
||||
.with_context(|| format!("Failed to read {entry:?}"))?;
|
||||
|
||||
let entry_file_name = entry.file_name().unwrap();
|
||||
let target_file_path = to.join(entry_file_name);
|
||||
|
||||
// If the entry is a dir and the recursive option is enabled, call itself
|
||||
if metadata.is_dir() && recursive {
|
||||
if entry.path() == to.to_path_buf() {
|
||||
if entry == to.as_os_str() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(avoid) = avoid_dir {
|
||||
if entry.path() == *avoid {
|
||||
if entry == *avoid {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// check if output dir already exists
|
||||
if !to.join(entry.file_name()).exists() {
|
||||
fs::create_dir(&to.join(entry.file_name()))?;
|
||||
if !target_file_path.exists() {
|
||||
fs::create_dir(&target_file_path)?;
|
||||
}
|
||||
|
||||
copy_files_except_ext(
|
||||
&from.join(entry.file_name()),
|
||||
&to.join(entry.file_name()),
|
||||
true,
|
||||
avoid_dir,
|
||||
ext_blacklist,
|
||||
)?;
|
||||
copy_files_except_ext(&entry, &target_file_path, true, avoid_dir, ext_blacklist)?;
|
||||
} else if metadata.is_file() {
|
||||
// Check if it is in the blacklist
|
||||
if let Some(ext) = entry.path().extension() {
|
||||
if let Some(ext) = entry.extension() {
|
||||
if ext_blacklist.contains(&ext.to_str().unwrap()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
"creating path for file: {:?}",
|
||||
&to.join(
|
||||
entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")
|
||||
)
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Copying {:?} to {:?}",
|
||||
entry.path(),
|
||||
&to.join(
|
||||
entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")
|
||||
)
|
||||
);
|
||||
fs::copy(
|
||||
entry.path(),
|
||||
&to.join(
|
||||
entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name..."),
|
||||
),
|
||||
)?;
|
||||
debug!("Copying {entry:?} to {target_file_path:?}");
|
||||
copy(&entry, &target_file_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies a file.
|
||||
fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
return copy_inner(from, to)
|
||||
.with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()));
|
||||
|
||||
// This is a workaround for an issue with the macOS file watcher.
|
||||
// Rust's `std::fs::copy` function uses `fclonefileat`, which creates
|
||||
// clones on APFS. Unfortunately fs events seem to trigger on both
|
||||
// sides of the clone, and there doesn't seem to be a way to differentiate
|
||||
// which side it is.
|
||||
// https://github.com/notify-rs/notify/issues/465#issuecomment-1657261035
|
||||
// contains more information.
|
||||
//
|
||||
// This is essentially a copy of the simple copy code path in Rust's
|
||||
// standard library.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn copy_inner(from: &Path, to: &Path) -> Result<()> {
|
||||
use std::fs::OpenOptions;
|
||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
|
||||
let mut reader = File::open(from)?;
|
||||
let metadata = reader.metadata()?;
|
||||
if !metadata.is_file() {
|
||||
anyhow::bail!(
|
||||
"expected a file, `{}` appears to be {:?}",
|
||||
from.display(),
|
||||
metadata.file_type()
|
||||
);
|
||||
}
|
||||
let perm = metadata.permissions();
|
||||
let mut writer = OpenOptions::new()
|
||||
.mode(perm.mode())
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(to)?;
|
||||
let writer_metadata = writer.metadata()?;
|
||||
if writer_metadata.is_file() {
|
||||
// Set the correct file permissions, in case the file already existed.
|
||||
// Don't set the permissions on already existing non-files like
|
||||
// pipes/FIFOs or device nodes.
|
||||
writer.set_permissions(perm)?;
|
||||
}
|
||||
std::io::copy(&mut reader, &mut writer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn copy_inner(from: &Path, to: &Path) -> Result<()> {
|
||||
fs::copy(from, to)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_404_output_file(input_404: &Option<String>) -> String {
|
||||
input_404
|
||||
.as_ref()
|
||||
@@ -206,69 +228,66 @@ mod tests {
|
||||
fn copy_files_except_ext_test() {
|
||||
let tmp = match tempfile::TempDir::new() {
|
||||
Ok(t) => t,
|
||||
Err(e) => panic!("Could not create a temp dir: {}", e),
|
||||
Err(e) => panic!("Could not create a temp dir: {e}"),
|
||||
};
|
||||
|
||||
// Create a couple of files
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("file.txt")) {
|
||||
panic!("Could not create file.txt: {}", err);
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.txt")) {
|
||||
panic!("Could not create file.txt: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("file.md")) {
|
||||
panic!("Could not create file.md: {}", err);
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.md")) {
|
||||
panic!("Could not create file.md: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("file.png")) {
|
||||
panic!("Could not create file.png: {}", err);
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.png")) {
|
||||
panic!("Could not create file.png: {err}");
|
||||
}
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir")) {
|
||||
panic!("Could not create sub_dir: {}", err);
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir")) {
|
||||
panic!("Could not create sub_dir: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
|
||||
panic!("Could not create sub_dir/file.png: {}", err);
|
||||
if let Err(err) = fs::File::create(tmp.path().join("sub_dir/file.png")) {
|
||||
panic!("Could not create sub_dir/file.png: {err}");
|
||||
}
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
|
||||
panic!("Could not create sub_dir_exists: {}", err);
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir_exists")) {
|
||||
panic!("Could not create sub_dir_exists: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
|
||||
panic!("Could not create sub_dir_exists/file.txt: {}", err);
|
||||
if let Err(err) = fs::File::create(tmp.path().join("sub_dir_exists/file.txt")) {
|
||||
panic!("Could not create sub_dir_exists/file.txt: {err}");
|
||||
}
|
||||
if let Err(err) = symlink(
|
||||
&tmp.path().join("file.png"),
|
||||
&tmp.path().join("symlink.png"),
|
||||
) {
|
||||
panic!("Could not symlink file.png: {}", err);
|
||||
if let Err(err) = symlink(tmp.path().join("file.png"), tmp.path().join("symlink.png")) {
|
||||
panic!("Could not symlink file.png: {err}");
|
||||
}
|
||||
|
||||
// Create output dir
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("output")) {
|
||||
panic!("Could not create output: {}", err);
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("output")) {
|
||||
panic!("Could not create output: {err}");
|
||||
}
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
|
||||
panic!("Could not create output/sub_dir_exists: {}", err);
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("output/sub_dir_exists")) {
|
||||
panic!("Could not create output/sub_dir_exists: {err}");
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])
|
||||
{
|
||||
panic!("Error while executing the function:\n{:?}", e);
|
||||
panic!("Error while executing the function:\n{e:?}");
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
if !(&tmp.path().join("output/file.txt")).exists() {
|
||||
if !tmp.path().join("output/file.txt").exists() {
|
||||
panic!("output/file.txt should exist")
|
||||
}
|
||||
if (&tmp.path().join("output/file.md")).exists() {
|
||||
if tmp.path().join("output/file.md").exists() {
|
||||
panic!("output/file.md should not exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/file.png")).exists() {
|
||||
if !tmp.path().join("output/file.png").exists() {
|
||||
panic!("output/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/sub_dir/file.png")).exists() {
|
||||
if !tmp.path().join("output/sub_dir/file.png").exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
|
||||
if !tmp.path().join("output/sub_dir_exists/file.txt").exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/symlink.png")).exists() {
|
||||
if !tmp.path().join("output/symlink.png").exists() {
|
||||
panic!("output/symlink.png should exist")
|
||||
}
|
||||
}
|
||||
|
||||
116
src/utils/mod.rs
116
src/utils/mod.rs
@@ -4,10 +4,11 @@ pub mod fs;
|
||||
mod string;
|
||||
pub(crate) mod toml_ext;
|
||||
use crate::errors::Error;
|
||||
use log::error;
|
||||
use once_cell::sync::Lazy;
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd};
|
||||
use regex::Regex;
|
||||
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
@@ -20,9 +21,7 @@ pub use self::string::{
|
||||
|
||||
/// Replaces multiple consecutive whitespace characters with a single space character.
|
||||
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"\s\s+").unwrap();
|
||||
}
|
||||
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s\s+").unwrap());
|
||||
RE.replace_all(text, " ")
|
||||
}
|
||||
|
||||
@@ -51,9 +50,7 @@ pub fn id_from_content(content: &str) -> String {
|
||||
let mut content = content.to_string();
|
||||
|
||||
// Skip any tags or html-encoded stuff
|
||||
lazy_static! {
|
||||
static ref HTML: Regex = Regex::new(r"(<.*?>)").unwrap();
|
||||
}
|
||||
static HTML: Lazy<Regex> = Lazy::new(|| Regex::new(r"(<.*?>)").unwrap());
|
||||
content = HTML.replace_all(&content, "").into();
|
||||
const REPL_SUB: &[&str] = &["<", ">", "&", "'", """];
|
||||
for sub in REPL_SUB {
|
||||
@@ -80,7 +77,7 @@ pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, us
|
||||
let id_count = id_counter.entry(id.clone()).or_insert(0);
|
||||
let unique_id = match *id_count {
|
||||
0 => id,
|
||||
id_count => format!("{}-{}", id, id_count),
|
||||
id_count => format!("{id}-{id_count}"),
|
||||
};
|
||||
*id_count += 1;
|
||||
unique_id
|
||||
@@ -96,10 +93,9 @@ pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, us
|
||||
/// None. Ideally, print page links would link to anchors on the print page,
|
||||
/// but that is very difficult.
|
||||
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
lazy_static! {
|
||||
static ref SCHEME_LINK: Regex = Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap();
|
||||
static ref MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap();
|
||||
}
|
||||
static SCHEME_LINK: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
|
||||
static MD_LINK: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap());
|
||||
|
||||
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
|
||||
if dest.starts_with('#') {
|
||||
@@ -109,7 +105,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
if base.ends_with(".md") {
|
||||
base.replace_range(base.len() - 3.., ".html");
|
||||
}
|
||||
return format!("{}{}", base, dest).into();
|
||||
return format!("{base}{dest}").into();
|
||||
} else {
|
||||
return dest;
|
||||
}
|
||||
@@ -125,7 +121,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
.to_str()
|
||||
.expect("utf-8 paths only");
|
||||
if !base.is_empty() {
|
||||
write!(fixed_link, "{}/", base).unwrap();
|
||||
write!(fixed_link, "{base}/").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,10 +148,8 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
// There are dozens of HTML tags/attributes that contain paths, so
|
||||
// feel free to add more tags if desired; these are the only ones I
|
||||
// care about right now.
|
||||
lazy_static! {
|
||||
static ref HTML_LINK: Regex =
|
||||
Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap();
|
||||
}
|
||||
static HTML_LINK: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap());
|
||||
|
||||
HTML_LINK
|
||||
.replace_all(&html, |caps: ®ex::Captures<'_>| {
|
||||
@@ -167,37 +161,59 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Start(Tag::Link(link_type, dest, title)) => {
|
||||
Event::Start(Tag::Link(link_type, fix(dest, path), title))
|
||||
}
|
||||
Event::Start(Tag::Image(link_type, dest, title)) => {
|
||||
Event::Start(Tag::Image(link_type, fix(dest, path), title))
|
||||
}
|
||||
Event::Start(Tag::Link {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
}) => Event::Start(Tag::Link {
|
||||
link_type,
|
||||
dest_url: fix(dest_url, path),
|
||||
title,
|
||||
id,
|
||||
}),
|
||||
Event::Start(Tag::Image {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
}) => Event::Start(Tag::Image {
|
||||
link_type,
|
||||
dest_url: fix(dest_url, path),
|
||||
title,
|
||||
id,
|
||||
}),
|
||||
Event::Html(html) => Event::Html(fix_html(html, path)),
|
||||
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
|
||||
_ => event,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
||||
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
render_markdown_with_path(text, curly_quotes, None)
|
||||
pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
|
||||
render_markdown_with_path(text, smart_punctuation, None)
|
||||
}
|
||||
|
||||
pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> {
|
||||
pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(Options::ENABLE_TABLES);
|
||||
opts.insert(Options::ENABLE_FOOTNOTES);
|
||||
opts.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
opts.insert(Options::ENABLE_TASKLISTS);
|
||||
if curly_quotes {
|
||||
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
||||
if smart_punctuation {
|
||||
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||
}
|
||||
Parser::new_ext(text, opts)
|
||||
}
|
||||
|
||||
pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String {
|
||||
pub fn render_markdown_with_path(
|
||||
text: &str,
|
||||
smart_punctuation: bool,
|
||||
path: Option<&Path>,
|
||||
) -> String {
|
||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||
let p = new_cmark_parser(text, curly_quotes);
|
||||
let p = new_cmark_parser(text, smart_punctuation);
|
||||
let events = p
|
||||
.map(clean_codeblock_headers)
|
||||
.map(|event| adjust_links(event, path))
|
||||
@@ -217,7 +233,7 @@ fn wrap_tables(event: Event<'_>) -> (Option<Event<'_>>, Option<Event<'_>>) {
|
||||
Some(Event::Html(r#"<div class="table-wrapper">"#.into())),
|
||||
Some(event),
|
||||
),
|
||||
Event::End(Tag::Table(_)) => (Some(event), Some(Event::Html(r#"</div>"#.into()))),
|
||||
Event::End(TagEnd::Table) => (Some(event), Some(Event::Html(r#"</div>"#.into()))),
|
||||
_ => (Some(event), None),
|
||||
}
|
||||
}
|
||||
@@ -249,6 +265,25 @@ pub fn log_backtrace(e: &Error) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn special_escape(mut s: &str) -> String {
|
||||
let mut escaped = String::with_capacity(s.len());
|
||||
let needs_escape: &[char] = &['<', '>', '\'', '\\', '&'];
|
||||
while let Some(next) = s.find(needs_escape) {
|
||||
escaped.push_str(&s[..next]);
|
||||
match s.as_bytes()[next] {
|
||||
b'<' => escaped.push_str("<"),
|
||||
b'>' => escaped.push_str(">"),
|
||||
b'\'' => escaped.push_str("'"),
|
||||
b'\\' => escaped.push_str("\"),
|
||||
b'&' => escaped.push_str("&"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
s = &s[next + 1..];
|
||||
}
|
||||
escaped.push_str(s);
|
||||
escaped
|
||||
}
|
||||
|
||||
pub(crate) fn bracket_escape(mut s: &str) -> String {
|
||||
let mut escaped = String::with_capacity(s.len());
|
||||
let needs_escape: &[char] = &['<', '>'];
|
||||
@@ -267,7 +302,7 @@ pub(crate) fn bracket_escape(mut s: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::bracket_escape;
|
||||
use super::{bracket_escape, special_escape};
|
||||
|
||||
mod render_markdown {
|
||||
use super::super::render_markdown;
|
||||
@@ -490,5 +525,20 @@ more text with spaces
|
||||
assert_eq!(bracket_escape("<>"), "<>");
|
||||
assert_eq!(bracket_escape("<test>"), "<test>");
|
||||
assert_eq!(bracket_escape("a<test>b"), "a<test>b");
|
||||
assert_eq!(bracket_escape("'"), "'");
|
||||
assert_eq!(bracket_escape("\\"), "\\");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_special() {
|
||||
assert_eq!(special_escape(""), "");
|
||||
assert_eq!(special_escape("<"), "<");
|
||||
assert_eq!(special_escape(">"), ">");
|
||||
assert_eq!(special_escape("<>"), "<>");
|
||||
assert_eq!(special_escape("<test>"), "<test>");
|
||||
assert_eq!(special_escape("a<test>b"), "a<test>b");
|
||||
assert_eq!(special_escape("'"), "'");
|
||||
assert_eq!(special_escape("\\"), "\");
|
||||
assert_eq!(special_escape("&"), "&");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::ops::Bound::{Excluded, Included, Unbounded};
|
||||
use std::ops::RangeBounds;
|
||||
@@ -23,10 +24,10 @@ pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref ANCHOR_START: Regex = Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap();
|
||||
static ref ANCHOR_END: Regex = Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap();
|
||||
}
|
||||
static ANCHOR_START: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap());
|
||||
static ANCHOR_END: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap());
|
||||
|
||||
/// Take anchored lines from a string.
|
||||
/// Lines containing anchor are ignored.
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
- [Tables](individual/table.md)
|
||||
- [Tasks](individual/task.md)
|
||||
- [Strikethrough](individual/strikethrough.md)
|
||||
- [MathJax](individual/mathjax.md)
|
||||
- [Mixed](individual/mixed.md)
|
||||
- [Languages](languages/README.md)
|
||||
- [Syntax Highlight](languages/highlight.md)
|
||||
|
||||
@@ -10,7 +10,9 @@ This is a codeblock
|
||||
|
||||
---
|
||||
|
||||
This line contains `inline code`
|
||||
This line contains `inline code` mixed with some other stuff. (LTR)
|
||||
|
||||
ושורה זו מכילה `inline code` אבל עם טקסט בשפה שנכתבת מימין לשמאל. (RTL)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -13,3 +13,9 @@
|
||||
##### Really Small Heading
|
||||
|
||||
###### Is it even a heading anymore - heading
|
||||
|
||||
## Custom id {#example-id}
|
||||
|
||||
## Custom class {.class1 .class2}
|
||||
|
||||
## Both id and class {#example-id2 .class1 .class2}
|
||||
|
||||
@@ -4,19 +4,19 @@ For copyright and trademark information on these images, please check [rust-artw
|
||||
|
||||
## A 16x16 image
|
||||
|
||||

|
||||

|
||||
|
||||
## A 32x32 image
|
||||
|
||||

|
||||

|
||||
|
||||
## A 256x256 image
|
||||
|
||||

|
||||

|
||||
|
||||
## A 512x512 image
|
||||
|
||||

|
||||

|
||||
|
||||
## A large image
|
||||
|
||||
|
||||
42
test_book/src/individual/mathjax.md
Normal file
42
test_book/src/individual/mathjax.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# MathJax
|
||||
|
||||
Fourier Transform
|
||||
|
||||
\\[
|
||||
\begin{aligned}
|
||||
f(x) &= \int_{-\infty}^{\infty}F(s)(-1)^{ 2xs}ds \\\\
|
||||
F(s) &= \int_{-\infty}^{\infty}f(x)(-1)^{-2xs}dx
|
||||
\end{aligned}
|
||||
\\]
|
||||
|
||||
The kernel can also be written as \\(e^{2i\pi xs}\\) which is more frequently used in literature.
|
||||
|
||||
> Proof that \\(e^{ix} = \cos x + i\sin x\\) a.k.a Euler's Formula:
|
||||
>
|
||||
> \\(
|
||||
\begin{aligned}
|
||||
e^x &= \sum_{n=0}^\infty \frac{x^n}{n!} \implies e^{ix} = \sum_{n=0}^\infty \frac{(ix)^n}{n!} \\\\
|
||||
\cos x &= \sum_{m=0}^\infty \frac{(-1)^m x^{2m}}{(2m)!} = \sum_{m=0}^\infty \frac{(ix)^{2m}}{(2m)!} \\\\
|
||||
\sin x &= \sum_{s=0}^\infty \frac{(-1)^s x^{2s+1}}{(2s+1)!} = \sum_{s=0}^\infty \frac{(ix)^{2s+1}}{i(2s+1)!} \\\\
|
||||
\cos x + i\sin x &= \sum_{l=0}^\infty \frac{(ix)^{2l}}{(2l)!} + \sum_{s=0}^\infty \frac{(ix)^{2s+1}}{(2s+1)!} = \sum_{n=0}^\infty \frac{(ix)^{n}}{n!} \\\\
|
||||
&= e^{ix}
|
||||
\end{aligned}
|
||||
\\)
|
||||
>
|
||||
|
||||
|
||||
Pauli Matrices
|
||||
|
||||
\\[
|
||||
\begin{aligned}
|
||||
\sigma_x &= \begin{pmatrix}
|
||||
1 & 0 \\\\ 0 & 1
|
||||
\end{pmatrix} \\\\
|
||||
\sigma_y &= \begin{pmatrix}
|
||||
0 & -i \\\\ i & 0
|
||||
\end{pmatrix} \\\\
|
||||
\sigma_z &= \begin{pmatrix}
|
||||
1 & 0 \\\\ 0 & -1
|
||||
\end{pmatrix}
|
||||
\end{aligned}
|
||||
\\]
|
||||
@@ -27,9 +27,11 @@ fn main(){
|
||||
}
|
||||
```
|
||||
|
||||
<kbd>Ctrl</kbd> + <kbd>S</kbd> saves a file.
|
||||
|
||||
A random image sprinkled in between
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -41,6 +43,7 @@ A random image sprinkled in between
|
||||
2. be
|
||||
3. `put`
|
||||
4. here?
|
||||
5. **<kbd>Ctrl</kbd> + <kbd>S</kbd> saves a file.**
|
||||
|
||||
| col1 | col2 | col 3 | col 4 | col 5 | col 6 |
|
||||
| ---- | ---- | ----- | ----- | ----- | ----- |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Strikethrough
|
||||
|
||||
~Single strike~
|
||||
|
||||
~~This is Striked~~
|
||||
|
||||
~~This is **strong**, _italic_ , **_both_** and striked~~
|
||||
|
||||
@@ -27,6 +27,8 @@ This Currently contains following languages
|
||||
- makefile
|
||||
- markdown
|
||||
- nginx
|
||||
- nim
|
||||
- nix
|
||||
- objectivec
|
||||
- perl
|
||||
- php
|
||||
|
||||
@@ -57,7 +57,7 @@ _start:
|
||||
|
||||
## bash
|
||||
|
||||
```
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
###### CONFIG
|
||||
@@ -529,6 +529,26 @@ http {
|
||||
var doors {.compileTime.}: array[1..numDoors, bool]
|
||||
|
||||
proc calcDoors(): string =
|
||||
for pass in 1..numDoors:
|
||||
for door in countup(pass, numDoors, pass):
|
||||
doors[door] = not doors[door]
|
||||
for door in 1..numDoors:
|
||||
result.add("Door $1 is $2.\n" % [$door, if doors[door]: "open" else: "closed"])
|
||||
|
||||
const outputString: string = calcDoors()
|
||||
|
||||
echo outputString
|
||||
```
|
||||
|
||||
## objectivec
|
||||
|
||||
```objectivec
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
@mylak {
|
||||
NSLog(@"Hello World!");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -543,6 +563,15 @@ int main(int argc, const char * argv[]) {
|
||||
"Hello " + world
|
||||
```
|
||||
|
||||
## perl
|
||||
|
||||
```perl
|
||||
print "Hello World!\n";
|
||||
```
|
||||
|
||||
## php
|
||||
|
||||
```php
|
||||
<?php
|
||||
echo "Hello World!";
|
||||
?>
|
||||
|
||||
@@ -39,29 +39,27 @@ fn alternate_backend_with_arguments() {
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
/// Get a command which will pipe `stdin` to the provided file.
|
||||
#[cfg(not(windows))]
|
||||
fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
|
||||
let out_file = out_file.as_ref();
|
||||
|
||||
if cfg!(windows) {
|
||||
format!("cmd.exe /c \"type > {}\"", out_file.display())
|
||||
} else {
|
||||
format!("tee {}", out_file.display())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn backends_receive_render_context_via_stdin() {
|
||||
use mdbook::renderer::RenderContext;
|
||||
use std::fs::File;
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("output").tempdir().unwrap();
|
||||
let out_file = temp.path().join("out.txt");
|
||||
let cmd = tee_command(&out_file);
|
||||
let (md, temp) = dummy_book_with_backend("cat-to-file", "renderers/myrenderer", false);
|
||||
|
||||
let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd, false);
|
||||
let renderers = temp.path().join("renderers");
|
||||
fs::create_dir(&renderers).unwrap();
|
||||
rust_exe(
|
||||
&renderers,
|
||||
"myrenderer",
|
||||
r#"fn main() {
|
||||
use std::io::Read;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_to_string(&mut s).unwrap();
|
||||
std::fs::write("out.txt", s).unwrap();
|
||||
}"#,
|
||||
);
|
||||
|
||||
let out_file = temp.path().join("book/out.txt");
|
||||
|
||||
assert!(!out_file.exists());
|
||||
md.build().unwrap();
|
||||
@@ -90,7 +88,7 @@ fn relative_command_path() {
|
||||
.set("output.html", toml::value::Table::new())
|
||||
.unwrap();
|
||||
config.set("output.myrenderer.command", cmd_path).unwrap();
|
||||
let md = MDBook::init(&temp.path())
|
||||
let md = MDBook::init(temp.path())
|
||||
.with_config(config)
|
||||
.build()
|
||||
.unwrap();
|
||||
@@ -119,13 +117,11 @@ fn dummy_book_with_backend(
|
||||
|
||||
let mut config = Config::default();
|
||||
config
|
||||
.set(format!("output.{}.command", name), command)
|
||||
.set(format!("output.{name}.command"), command)
|
||||
.unwrap();
|
||||
|
||||
if backend_is_optional {
|
||||
config
|
||||
.set(format!("output.{}.optional", name), true)
|
||||
.unwrap();
|
||||
config.set(format!("output.{name}.optional"), true).unwrap();
|
||||
}
|
||||
|
||||
let md = MDBook::init(temp.path())
|
||||
|
||||
24
tests/cli/init.rs
Normal file
24
tests/cli/init.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use crate::cli::cmd::mdbook_cmd;
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
||||
use mdbook::config::Config;
|
||||
|
||||
/// Run `mdbook init` with `--force` to skip the confirmation prompts
|
||||
#[test]
|
||||
fn base_mdbook_init_can_skip_confirmation_prompts() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
|
||||
// doesn't exist before
|
||||
assert!(!temp.path().join("book").exists());
|
||||
|
||||
let mut cmd = mdbook_cmd();
|
||||
cmd.args(["init", "--force"]).current_dir(temp.path());
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(predicates::str::contains("\nAll done, no errors...\n"));
|
||||
|
||||
let config = Config::from_disk(temp.path().join("book.toml")).unwrap();
|
||||
assert_eq!(config.book.title, None);
|
||||
|
||||
assert!(!temp.path().join(".gitignore").exists());
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user