mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 17:21:52 -05:00
Compare commits
384 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b29f8a717 | ||
|
|
94569a42da | ||
|
|
97747621aa | ||
|
|
723d9df6c7 | ||
|
|
45e2158e84 | ||
|
|
59017ea918 | ||
|
|
f857ab294c | ||
|
|
938a9126b0 | ||
|
|
23724b0a6b | ||
|
|
7bdea7c085 | ||
|
|
2bcae6b0a9 | ||
|
|
6457b381d8 | ||
|
|
9eb6fe0483 | ||
|
|
674733864f | ||
|
|
3d39bf4adb | ||
|
|
55830b9fca | ||
|
|
8bd3c52abf | ||
|
|
de8202a67f | ||
|
|
8f277a0d39 | ||
|
|
d63aeb6526 | ||
|
|
eb83d080f6 | ||
|
|
710ec2755d | ||
|
|
5d64d0e5f2 | ||
|
|
718ceecfa2 | ||
|
|
09616e31af | ||
|
|
7e7a3b495e | ||
|
|
d39deca76a | ||
|
|
8385e750ec | ||
|
|
8571d70b52 | ||
|
|
4b5ea14ee1 | ||
|
|
c53379e3ac | ||
|
|
6bf7fadc29 | ||
|
|
d193775a3b | ||
|
|
8e4bc4aecd | ||
|
|
2afad43bdd | ||
|
|
5445458d1a | ||
|
|
ef476a7329 | ||
|
|
262afdc2f8 | ||
|
|
ef10e720a5 | ||
|
|
fff6087f36 | ||
|
|
922f0d8ad4 | ||
|
|
ab1325b213 | ||
|
|
63b159741b | ||
|
|
4a9a517f27 | ||
|
|
700839f77f | ||
|
|
152132458e | ||
|
|
054da77b6a | ||
|
|
9a5e8dbb0a | ||
|
|
63c45cd879 | ||
|
|
bc7ca458b6 | ||
|
|
07fb33f5da | ||
|
|
7d1566860c | ||
|
|
f0117ec3df | ||
|
|
22065ebc79 | ||
|
|
5905bf1d85 | ||
|
|
1646e4923a | ||
|
|
1e190137c3 | ||
|
|
4417f8cb0a | ||
|
|
cc7f8be496 | ||
|
|
051fc9f01d | ||
|
|
d0bde467e0 | ||
|
|
475951c9ee | ||
|
|
5b2cc1735b | ||
|
|
1c00395230 | ||
|
|
9799326590 | ||
|
|
2546c8cc60 | ||
|
|
33c9b4063e | ||
|
|
f66fd92f32 | ||
|
|
541e16335b | ||
|
|
6cc40cb5f7 | ||
|
|
37f8a79d4d | ||
|
|
385246a9ef | ||
|
|
7619f9a91c | ||
|
|
f27c3aea4c | ||
|
|
b3bd103742 | ||
|
|
7e5fa3565b | ||
|
|
6d9f49cbc5 | ||
|
|
8670bcc540 | ||
|
|
005f4d648a | ||
|
|
59343b525d | ||
|
|
e8d7dd6f57 | ||
|
|
54175698d5 | ||
|
|
07ed00e8f7 | ||
|
|
ab8a4dfa5a | ||
|
|
e2c954f693 | ||
|
|
b2111a3f91 | ||
|
|
8ba833feb2 | ||
|
|
7124f4c7de | ||
|
|
1cc4cbb202 | ||
|
|
405f407260 | ||
|
|
eaa778bebd | ||
|
|
a17c1d1b95 | ||
|
|
8a27d1b7ac | ||
|
|
d6cd50b601 | ||
|
|
68d9bcfec4 | ||
|
|
3fa49214ad | ||
|
|
ddf02e0c0c | ||
|
|
3992bc18f5 | ||
|
|
49f9c9741e | ||
|
|
f84b1a15b6 | ||
|
|
ac11e00aa2 | ||
|
|
860e8d109e | ||
|
|
adcbd117da | ||
|
|
08d9fddfc9 | ||
|
|
118c1096ea | ||
|
|
18813516e1 | ||
|
|
a6944683e6 | ||
|
|
f27ae21825 | ||
|
|
58af25384d | ||
|
|
51a80febb3 | ||
|
|
7945ac8e8c | ||
|
|
a097fe6232 | ||
|
|
4b5004b621 | ||
|
|
7b7dee4a4e | ||
|
|
5282083dec | ||
|
|
816913bd72 | ||
|
|
1620858032 | ||
|
|
4a07076896 | ||
|
|
3a2705d742 | ||
|
|
b0229e76a5 | ||
|
|
a43ef4ca4b | ||
|
|
780fd83cac | ||
|
|
d5406c8dff | ||
|
|
5e7b6d7d9d | ||
|
|
07d2989486 | ||
|
|
5fe7e9531d | ||
|
|
f958a0e8cf | ||
|
|
dbad189b26 | ||
|
|
4a06e067c5 | ||
|
|
98a093a0ff | ||
|
|
9b5c57bf48 | ||
|
|
87e9cc0ac3 | ||
|
|
e37e5314f8 | ||
|
|
4913bf82f1 | ||
|
|
4e41c844c3 | ||
|
|
0ae202ea85 | ||
|
|
71083144e8 | ||
|
|
7eb63bf0e3 | ||
|
|
283b2798e4 | ||
|
|
91842c3db6 | ||
|
|
fd88719b68 | ||
|
|
c438e13027 | ||
|
|
8e9697bd8b | ||
|
|
dcf7ae45c2 | ||
|
|
38fa5b5ebc | ||
|
|
41262665e8 | ||
|
|
21dc3b0eb1 | ||
|
|
44cd371066 | ||
|
|
1c766160c3 | ||
|
|
2cb4093cc8 | ||
|
|
dad941454e | ||
|
|
2a93606727 | ||
|
|
78819f4525 | ||
|
|
2c7d192b50 | ||
|
|
e15f80407d | ||
|
|
4fc72e8d9f | ||
|
|
b4c53b9e9c | ||
|
|
2011ddb479 | ||
|
|
4a28995641 | ||
|
|
4c397a9be0 | ||
|
|
f1b413444b | ||
|
|
cd1b54f41f | ||
|
|
83c307be3c | ||
|
|
9ec49d978f | ||
|
|
7dee816838 | ||
|
|
5b79ed4144 | ||
|
|
aa96e1174e | ||
|
|
7fcacf3386 | ||
|
|
2aa2b95f0f | ||
|
|
797112ef36 | ||
|
|
f24221a1d7 | ||
|
|
6223189b95 | ||
|
|
73aeed48d7 | ||
|
|
09c3e542da | ||
|
|
9fae3c88eb | ||
|
|
8159dea9ea | ||
|
|
e5386f9230 | ||
|
|
5fafc3f1f6 | ||
|
|
39a148fc8d | ||
|
|
873e4fe40f | ||
|
|
604d4dd78a | ||
|
|
ddeb3ce54f | ||
|
|
15958773d5 | ||
|
|
ba4c3ed873 | ||
|
|
53d39a8654 | ||
|
|
f1731329e1 | ||
|
|
51d7998ba4 | ||
|
|
d27a2bdd1d | ||
|
|
cd3e26fb90 | ||
|
|
1c034bdd9a | ||
|
|
2b242494b0 | ||
|
|
d4763d2c90 | ||
|
|
03443f723c | ||
|
|
c3f4b8114a | ||
|
|
21e8c08827 | ||
|
|
de155f859b | ||
|
|
737090abf1 | ||
|
|
fb4fa867d1 | ||
|
|
c606a010c7 | ||
|
|
09f05e8a86 | ||
|
|
1daa650d61 | ||
|
|
2474ae799b | ||
|
|
f393e22896 | ||
|
|
3629e2c051 | ||
|
|
e7b15274b5 | ||
|
|
d15a40123c | ||
|
|
166a972e9a | ||
|
|
f2db034587 | ||
|
|
a4140ba535 | ||
|
|
e3bb655663 | ||
|
|
8bb9a7ff42 | ||
|
|
3e673ce424 | ||
|
|
787882069e | ||
|
|
e6fffcf9ec | ||
|
|
c78d463864 | ||
|
|
14f249071c | ||
|
|
0dc65a1ac4 | ||
|
|
8f46ad8575 | ||
|
|
6dd8be2ab2 | ||
|
|
29b71be0a5 | ||
|
|
f3bb6ce7ff | ||
|
|
0ce670d124 | ||
|
|
ebcd293fee | ||
|
|
5eaae38bf4 | ||
|
|
be63b44038 | ||
|
|
30d3aeb691 | ||
|
|
06af133838 | ||
|
|
327417373f | ||
|
|
1b55d4a389 | ||
|
|
ac1674845f | ||
|
|
148d282065 | ||
|
|
c0dc4b7367 | ||
|
|
6f3fac763c | ||
|
|
73a1652b64 | ||
|
|
9b5e5e7c0f | ||
|
|
d071d127ef | ||
|
|
321a76bd27 | ||
|
|
8c5b72ca3f | ||
|
|
3e421d353d | ||
|
|
3cd055c508 | ||
|
|
189eea5beb | ||
|
|
3f45b024f2 | ||
|
|
a40f1f281b | ||
|
|
6c847275c5 | ||
|
|
aced9f609c | ||
|
|
82a7df41a6 | ||
|
|
f6c11d12e0 | ||
|
|
313be7162f | ||
|
|
800fb54aeb | ||
|
|
45e700db00 | ||
|
|
24c7ffcd62 | ||
|
|
ec436adca2 | ||
|
|
b8ad85c16f | ||
|
|
6be8e526d6 | ||
|
|
f4012757a7 | ||
|
|
0722d81295 | ||
|
|
402d11414c | ||
|
|
988ed9b5bc | ||
|
|
4a47b3d18a | ||
|
|
82a457b548 | ||
|
|
54a5d749fc | ||
|
|
c177081104 | ||
|
|
1b00525574 | ||
|
|
dbb51d32db | ||
|
|
b4641d2830 | ||
|
|
03f2806cae | ||
|
|
4498739095 | ||
|
|
29ae40c3ec | ||
|
|
6746df7ce9 | ||
|
|
a0a01ecd60 | ||
|
|
21f2435182 | ||
|
|
338a9b424e | ||
|
|
25c47ed0bc | ||
|
|
0f0d1f3377 | ||
|
|
ae1a8c362c | ||
|
|
0043043bb3 | ||
|
|
e9e3bb6ddd | ||
|
|
03ba7d9089 | ||
|
|
d7892f5601 | ||
|
|
0a29ba6eb6 | ||
|
|
4d9095b603 | ||
|
|
235c1f87f0 | ||
|
|
5d44ef91dc | ||
|
|
e7084e5548 | ||
|
|
4637d5f5d1 | ||
|
|
4bac54883b | ||
|
|
2dc8c5e686 | ||
|
|
7b3e6973be | ||
|
|
9fce4ad74c | ||
|
|
76c5b6967a | ||
|
|
04ff12a206 | ||
|
|
3236ec0aea | ||
|
|
ff5e85af51 | ||
|
|
c1b631d086 | ||
|
|
39ce6123b6 | ||
|
|
01da420f76 | ||
|
|
df037d132d | ||
|
|
534725cbb8 | ||
|
|
4c948b4547 | ||
|
|
bba1216df1 | ||
|
|
3e5ec749ba | ||
|
|
7e0949175a | ||
|
|
b51d3e5106 | ||
|
|
f47066f68f | ||
|
|
9ac0eb288a | ||
|
|
79e9ae48a1 | ||
|
|
d29072783f | ||
|
|
e284eb1c30 | ||
|
|
b09c588ca9 | ||
|
|
a6a60eef31 | ||
|
|
95128b6662 | ||
|
|
824e2a7681 | ||
|
|
43e690dd13 | ||
|
|
4a2d3a7e85 | ||
|
|
eda77b81db | ||
|
|
8befcc74d1 | ||
|
|
5956092b4b | ||
|
|
c25e866796 | ||
|
|
1d1274e53a | ||
|
|
841c68d05e | ||
|
|
37273ba8e0 | ||
|
|
91f04bc7ba | ||
|
|
7cce45818a | ||
|
|
84fbd679e7 | ||
|
|
aef12bbb20 | ||
|
|
00eba964ce | ||
|
|
cf7762f57f | ||
|
|
26319f4124 | ||
|
|
f8e66db41c | ||
|
|
529cfc34ec | ||
|
|
8053774ba3 | ||
|
|
fe76ee626f | ||
|
|
f6c062fc98 | ||
|
|
97d9078a32 | ||
|
|
a397f64356 | ||
|
|
fad53f720f | ||
|
|
dcfb527342 | ||
|
|
e0a4fb1ea9 | ||
|
|
6e6518a7ae | ||
|
|
12fc0ff5c3 | ||
|
|
b8a7b6e846 | ||
|
|
9229e80499 | ||
|
|
ae6c4522bb | ||
|
|
fdebbfdce2 | ||
|
|
40745600a3 | ||
|
|
5a31947eb7 | ||
|
|
d758753551 | ||
|
|
9b27b14985 | ||
|
|
f5fc54461a | ||
|
|
6aac696ee1 | ||
|
|
c8571f592c | ||
|
|
7eccd1d556 | ||
|
|
06324d8b24 | ||
|
|
753780f653 | ||
|
|
3087686559 | ||
|
|
6805740e20 | ||
|
|
8f3b6b4776 | ||
|
|
3278f84373 | ||
|
|
12285f505d | ||
|
|
e123879c8c | ||
|
|
7bcdfe6f0f | ||
|
|
29f936b1eb | ||
|
|
bd3e555962 | ||
|
|
02b6628048 | ||
|
|
4ae5a53791 | ||
|
|
fc76a47d6e | ||
|
|
a224bfd7d7 | ||
|
|
f51d89ba02 | ||
|
|
461884f109 | ||
|
|
bc3399cc22 | ||
|
|
4a655ff2a3 | ||
|
|
877a3af671 | ||
|
|
1499e850ac | ||
|
|
c7b67e363b | ||
|
|
d5a505e0c6 | ||
|
|
0de13cf5a9 | ||
|
|
d6d5d6e674 | ||
|
|
5264074c1b | ||
|
|
702c676107 | ||
|
|
a387846b20 | ||
|
|
0eabdb0169 | ||
|
|
92836f3988 | ||
|
|
05c6a99446 | ||
|
|
68893f785f |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
xtask = "run --manifest-path=crates/xtask/Cargo.toml --"
|
||||
@@ -1,95 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"globals": {
|
||||
"module": "readonly",
|
||||
"require": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2021,
|
||||
"requireConfigFile": false,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"ignorePatterns": ["**min.js", "**/highlight.js", "**/playground_editor/*"],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"brace-style": [
|
||||
"error",
|
||||
"1tbs",
|
||||
{ "allowSingleLine": false }
|
||||
],
|
||||
"curly": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"keyword-spacing": [
|
||||
"error",
|
||||
{ "before": true, "after": true }
|
||||
],
|
||||
"comma-spacing": [
|
||||
"error",
|
||||
{ "before": false, "after": true }
|
||||
],
|
||||
"arrow-spacing": [
|
||||
"error",
|
||||
{ "before": true, "after": true }
|
||||
],
|
||||
"key-spacing": [
|
||||
"error",
|
||||
{ "beforeColon": false, "afterColon": true, "mode": "strict" }
|
||||
],
|
||||
"func-call-spacing": ["error", "never"],
|
||||
"space-infix-ops": "error",
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"space-before-blocks": "error",
|
||||
"no-console": [
|
||||
"error",
|
||||
{ "allow": ["warn", "error"] }
|
||||
],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-style": ["error", "last"],
|
||||
"max-len": ["error", { "code": 100, "tabWidth": 2 }],
|
||||
"eol-last": ["error", "always"],
|
||||
"no-extra-parens": "error",
|
||||
"arrow-parens": ["error", "as-needed"],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"prefer-const": ["error"],
|
||||
"no-var": "error",
|
||||
"eqeqeq": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"tests/**/*.js"
|
||||
],
|
||||
"env": {
|
||||
"jest": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
26
.git-blame-ignore-revs
Normal file
26
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,26 @@
|
||||
# Use `git config blame.ignorerevsfile .git-blame-ignore-revs` to make `git blame` ignore the following commits.
|
||||
|
||||
# rustfmt
|
||||
ad0794a0bd692e4f2ff23b85e361889620e93f51
|
||||
# rustfmt and use_try_shorthand
|
||||
75bbd55128083897d40c3f5265cc5b1f10314ddb
|
||||
# rustfmt
|
||||
382fc4139b96bde3c4b8875b499c720eabc89c6a
|
||||
# rustfmt
|
||||
154e0fb3080c6ffc225b0d47b5d835e589789892
|
||||
# rustfmt
|
||||
5835da243244bfc5c95c6c6db96f453da4bb5740
|
||||
# rustfmt
|
||||
fd9d27e082f5e9eea50e4fa9fa3a22060d02c66b
|
||||
# rustfmt
|
||||
1d69ccae4854f13552d452d0bffef95cbff70364
|
||||
# rustfmt
|
||||
3688f73052454bf510a5acc85cf55aae450c6e46
|
||||
# rustfmt
|
||||
742dbbc91700dce1b7d910bca6b3e10a5ae46b86
|
||||
# rustfmt 1.38
|
||||
b88839cc25a6fd1c782101e94318959e8079bb20
|
||||
# rustfmt 1.40
|
||||
2f59943c04f0aa204a9238d6a699ba9cc06c88d9
|
||||
# Rustfmt for 2024
|
||||
c7b67e363bb9ce3383636ee615e8e761bf185b33
|
||||
71
.github/renovate.json5
vendored
Normal file
71
.github/renovate.json5
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
schedule: ['before 5am on the first day of the month'],
|
||||
// Raise from default of 2 to reduce trickle.
|
||||
prHourlyLimit: 6,
|
||||
dependencyDashboard: true,
|
||||
// Creates PRs if this renovate config file needs updating.
|
||||
configMigration: true,
|
||||
ignorePaths: [
|
||||
'guide/src/for_developers/mdbook-wordcount/',
|
||||
],
|
||||
customManagers: [
|
||||
// Custom manager to extract the version of cargo-semver-checks from the workflow.
|
||||
{
|
||||
customType: 'regex',
|
||||
managerFilePatterns: [
|
||||
'/^.github.workflows.main.yml$/',
|
||||
],
|
||||
matchStrings: [
|
||||
'cargo-semver-checks.releases.download.v(?<currentValue>\\d+\\.\\d+(\\.\\d+)?)',
|
||||
],
|
||||
depNameTemplate: 'cargo-semver-checks',
|
||||
packageNameTemplate: 'obi1kenobi/cargo-semver-checks',
|
||||
datasourceTemplate: 'github-releases',
|
||||
},
|
||||
],
|
||||
packageRules: [
|
||||
// The next two rules disable compatible dependency updates. I wasn't
|
||||
// able to get Renovate to be able to update Cargo.toml for compatible
|
||||
// updates only, update all transitive dependencies, and do that all
|
||||
// in a single PR. Instead, the `update-dependencies.sh` will handle
|
||||
// that.
|
||||
{
|
||||
matchManagers: ['cargo'],
|
||||
matchUpdateTypes: ['patch'],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
matchManagers: ['cargo'],
|
||||
matchCurrentVersion: '>=1.0.0',
|
||||
matchUpdateTypes: ['minor'],
|
||||
enabled: false,
|
||||
},
|
||||
// Allow minor updates for pre-1.0 dependencies (semver-breaking)
|
||||
{
|
||||
matchManagers: ['cargo'],
|
||||
matchCurrentVersion: '<1.0.0',
|
||||
matchUpdateTypes: ['minor'],
|
||||
},
|
||||
// Allow major updates for stable dependencies (semver-breaking)
|
||||
{
|
||||
matchManagers: ['cargo'],
|
||||
matchCurrentVersion: '>=1.0.0',
|
||||
matchUpdateTypes: ['major'],
|
||||
},
|
||||
// Update cargo-semver-checks when a new version is available.
|
||||
{
|
||||
commitMessageTopic: 'cargo-semver-checks',
|
||||
matchManagers: [
|
||||
'custom.regex',
|
||||
],
|
||||
matchDepNames: [
|
||||
'cargo-semver-checks',
|
||||
],
|
||||
extractVersion: '^v(?<version>\\d+\\.\\d+\\.\\d+)',
|
||||
schedule: [
|
||||
'* * * * *',
|
||||
],
|
||||
internalChecksFilter: 'strict',
|
||||
},
|
||||
]
|
||||
}
|
||||
27
.github/workflows/deploy.yml
vendored
27
.github/workflows/deploy.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
os: windows-latest
|
||||
name: Deploy ${{ matrix.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: ci/install-rust.sh stable ${{ matrix.target }}
|
||||
- name: Build asset
|
||||
@@ -43,27 +43,26 @@ jobs:
|
||||
name: GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Build book
|
||||
run: cargo run -- build guide
|
||||
- name: Deploy the User Guide to GitHub Pages using the gh-pages branch
|
||||
env:
|
||||
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
|
||||
run: |
|
||||
touch guide/book/.nojekyll
|
||||
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
|
||||
cd guide/book
|
||||
/tmp/deploy
|
||||
run: ci/publish-guide.sh
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Required for OIDC token exchange
|
||||
id-token: write
|
||||
environment: publish
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Authenticate with crates.io
|
||||
id: auth
|
||||
uses: rust-lang/crates-io-auth-action@v1
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo publish --no-verify
|
||||
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
|
||||
run: cargo publish --workspace --no-verify
|
||||
|
||||
37
.github/workflows/main.yml
vendored
37
.github/workflows/main.yml
vendored
@@ -40,22 +40,22 @@ jobs:
|
||||
- name: msrv
|
||||
os: ubuntu-22.04
|
||||
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
|
||||
rust: 1.82.0
|
||||
rust: 1.88.0
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: ${{ matrix.name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh ${{ matrix.rust }} ${{ matrix.target }}
|
||||
- name: Build and run tests
|
||||
run: cargo test --locked --target ${{ matrix.target }}
|
||||
run: cargo test --workspace --locked --target ${{ matrix.target }}
|
||||
- name: Test no default
|
||||
run: cargo test --no-default-features --target ${{ matrix.target }}
|
||||
run: cargo test --workspace --no-default-features --target ${{ matrix.target }}
|
||||
|
||||
aarch64-cross-builds:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable aarch64-unknown-linux-musl
|
||||
- name: Build
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt --check
|
||||
@@ -74,13 +74,13 @@ jobs:
|
||||
name: GUI tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- name: Install npm
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
- name: Install browser-ui-test
|
||||
run: npm install
|
||||
- name: Run eslint
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- run: rustup component add clippy
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
name: Check API docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- name: Ensure intradoc links are valid
|
||||
@@ -110,6 +110,20 @@ jobs:
|
||||
env:
|
||||
RUSTDOCFLAGS: -D warnings
|
||||
|
||||
check-version-bump:
|
||||
name: Check version bump
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: rustup update stable && rustup default stable
|
||||
- name: Install cargo-semver-checks
|
||||
run: |
|
||||
mkdir installed-bins
|
||||
curl -Lf https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.45.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz \
|
||||
| tar -xz --directory=./installed-bins
|
||||
echo `pwd`/installed-bins >> $GITHUB_PATH
|
||||
- run: cargo semver-checks --workspace
|
||||
|
||||
# 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
|
||||
@@ -125,6 +139,7 @@ jobs:
|
||||
- gui
|
||||
- clippy
|
||||
- docs
|
||||
- check-version-bump
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'
|
||||
|
||||
21
.github/workflows/update-dependencies.yml
vendored
Normal file
21
.github/workflows/update-dependencies.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Update dependencies
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update:
|
||||
name: Update dependencies
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'rust-lang/mdBook'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- name: Install cargo-edit
|
||||
run: cargo install cargo-edit --locked
|
||||
- name: Update dependencies
|
||||
run: ci/update-dependencies.sh
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,7 +8,7 @@ guide/book
|
||||
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
test_book/book/
|
||||
tests/gui/books/*/book/
|
||||
tests/testsuite/*/*/book/
|
||||
|
||||
# Ignore Jetbrains specific files.
|
||||
|
||||
349
CHANGELOG.md
349
CHANGELOG.md
@@ -1,5 +1,354 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.5.2
|
||||
[v0.5.1...v0.5.2](https://github.com/rust-lang/mdBook/compare/v0.5.1...v0.5.2)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated Rust crate html5ever to 0.36.0.
|
||||
[#2970](https://github.com/rust-lang/mdBook/pull/2970)
|
||||
- Updated cargo dependencies.
|
||||
[#2969](https://github.com/rust-lang/mdBook/pull/2969)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed repeated error message when HTML config is invalid in `mdbook serve`.
|
||||
[#2983](https://github.com/rust-lang/mdBook/pull/2983)
|
||||
- Fixed sidebar scroll position when heading nav is involved.
|
||||
[#2982](https://github.com/rust-lang/mdBook/pull/2982)
|
||||
- Fixed color for rustdoc error messages.
|
||||
[#2981](https://github.com/rust-lang/mdBook/pull/2981)
|
||||
- Fixed usage of custom preprocessors with `MDBook::test`.
|
||||
[#2980](https://github.com/rust-lang/mdBook/pull/2980)
|
||||
|
||||
## mdBook 0.5.1
|
||||
[v0.5.0...v0.5.1](https://github.com/rust-lang/mdBook/compare/v0.5.0...v0.5.1)
|
||||
|
||||
### Changed
|
||||
- Changed the scrollbar background to be transparent.
|
||||
[#2932](https://github.com/rust-lang/mdBook/pull/2932)
|
||||
- Ignore invalid top-level environment variable config keys. This allows setting things like `MDBOOK_VERSION` to not cause an error.
|
||||
[#2952](https://github.com/rust-lang/mdBook/pull/2952)
|
||||
|
||||
### Fixed
|
||||
- Fixed the sidebar heading nav to have the correct nesting levels.
|
||||
[#2953](https://github.com/rust-lang/mdBook/pull/2953)
|
||||
- Various Font Awesome fixes and improvements.
|
||||
[#2951](https://github.com/rust-lang/mdBook/pull/2951)
|
||||
|
||||
## mdBook 0.5.0
|
||||
[v0.4.52...v0.5.0](https://github.com/rust-lang/mdBook/compare/v0.4.52...v0.5.0)
|
||||
|
||||
The 0.5.0 release is the next major release of mdBook, containing over 130 PRs since 0.4.52! The primary focus for this release has been an evolution of the Rust APIs to make it easier to maintain, to evolve in a backwards-compatible fashion, to clean up some things that have accumulated over time, and to significantly improve the performance and compile-times.
|
||||
|
||||
This release also includes many new features described below.
|
||||
|
||||
We have prepared a [0.5 Migration Guide](#05-migration-guide) to help existing authors switch from 0.4.
|
||||
|
||||
The final 0.5.0 release only contains the following changes since [0.5.0-beta.2](#mdbook-050-beta2):
|
||||
|
||||
- Added error handling to environment config handling. This checks that environment variables starting with `MDBOOK_` are correctly specified instead of silently ignoring. This also fixed being able to replace entire top-level tables like `MDBOOK_OUTPUT`.
|
||||
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
|
||||
|
||||
## 0.5 Migration Guide
|
||||
|
||||
The 0.5 release contains several breaking changes from the 0.4 release. Preprocessors and renderers will need to be migrated to continue to work with this release. After updating your configuration, it is recommended to carefully compare and review how your book renders to ensure everything is working correctly.
|
||||
|
||||
If you have overridden any of the theme files, you will likely need to update them to match the current version.
|
||||
|
||||
See the entries below for [mdBook 0.5.0-alpha.1](#mdbook-050-alpha1), [mdBook 0.5.0-beta.1](#mdbook-050-beta1), and [mdBook 0.5.0-beta.2](#mdbook-050-beta2) for a more complete list of changes and fixes.
|
||||
|
||||
The following is a summary of the changes that may require your attention when updating to 0.5:
|
||||
|
||||
### Major additions
|
||||
|
||||
- Added sidebar heading navigation. This includes the `output.html.sidebar-header-nav` option to disable it.
|
||||
[#2822](https://github.com/rust-lang/mdBook/pull/2822)
|
||||
- Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it. See [docs](https://rust-lang.github.io/mdBook/format/markdown.html#definition-lists) for more.
|
||||
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
|
||||
- Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it. See [docs](https://rust-lang.github.io/mdBook/format/markdown.html#admonitions) for more.
|
||||
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
|
||||
- Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
|
||||
### Config changes
|
||||
|
||||
- Unknown fields in config are now an error.
|
||||
[#2787](https://github.com/rust-lang/mdBook/pull/2787)
|
||||
[#2801](https://github.com/rust-lang/mdBook/pull/2801)
|
||||
- Removed `curly-quotes`, use `output.html.smart-punctuation` instead.
|
||||
[#2788](https://github.com/rust-lang/mdBook/pull/2788)
|
||||
- Removed `output.html.copy-fonts`. The default fonts are now always copied unless you override the `theme/fonts/fonts.css` file.
|
||||
[#2790](https://github.com/rust-lang/mdBook/pull/2790)
|
||||
- If the `command` path for a renderer or preprocessor is relative, it is now always relative to the book root.
|
||||
[#2792](https://github.com/rust-lang/mdBook/pull/2792)
|
||||
[#2796](https://github.com/rust-lang/mdBook/pull/2796)
|
||||
- Added the `optional` field for preprocessors. The default is `false`, so this also means it is an error by default if the preprocessor is missing.
|
||||
[#2797](https://github.com/rust-lang/mdBook/pull/2797)
|
||||
- `output.html.smart-punctuation` is now `true` by default.
|
||||
[#2810](https://github.com/rust-lang/mdBook/pull/2810)
|
||||
- `output.html.hash-files` is now `true` by default.
|
||||
[#2820](https://github.com/rust-lang/mdBook/pull/2820)
|
||||
- Removed support for google-analytics. Use a theme extension (like `head.hbs`) if you need to continue to support this.
|
||||
[#2776](https://github.com/rust-lang/mdBook/pull/2776)
|
||||
- Removed the `book.multilingual` field. This was never used.
|
||||
[#2775](https://github.com/rust-lang/mdBook/pull/2775)
|
||||
- Removed the very old legacy config support. Warnings have been displayed in previous versions on how to migrate.
|
||||
[#2783](https://github.com/rust-lang/mdBook/pull/2783)
|
||||
- Top-level config values set from the environment like `MDBOOK_BOOK` now *replace* the contents of the top-level table instead of merging into it.
|
||||
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
|
||||
- Invalid environment variables are now rejected. Previously unknown keys like `MDBOOK_FOO` would be ignored, or keys or invalid values inside objects like the `[book]` table would be ignored.
|
||||
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
|
||||
|
||||
### Theme changes
|
||||
|
||||
- Replaced the `{{#previous}}` and `{{#next}}` handlebars helpers with simple objects that contain the previous and next values.
|
||||
[#2794](https://github.com/rust-lang/mdBook/pull/2794)
|
||||
- Removed the `{{theme_option}}` handlebars helper. It has not been used for a while.
|
||||
[#2795](https://github.com/rust-lang/mdBook/pull/2795)
|
||||
|
||||
### Rendering changes
|
||||
|
||||
- Updated to a newer version of `pulldown-cmark`. This brings a large number of fixes to markdown processing.
|
||||
[#2401](https://github.com/rust-lang/mdBook/pull/2401)
|
||||
- The font-awesome font is no longer loaded as a font. Instead, the corresponding SVG is embedded in the output for the corresponding `<i>` tags. Additionally, a handlebars helper has been added for the `hbs` files. This also updates the version from 4.7.0 to 6.2.0, which means some of the icon names and styles have changed. Most of the free icons are in the "solid" set. See the [free icon set](https://fontawesome.com/v6/search) for the available icons.
|
||||
[#1330](https://github.com/rust-lang/mdBook/pull/1330)
|
||||
- Changed all internal HTML IDs to have an `mdbook-` prefix. This helps avoid namespace conflicts with header IDs.
|
||||
[#2808](https://github.com/rust-lang/mdBook/pull/2808)
|
||||
- There is a new internal HTML rendering pipeline. This is primarily intended to give mdBook more flexibility in generating its HTML output. This resulted in some small changes to the HTML structure. HTML parsing may now be more strict than before.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it.
|
||||
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
|
||||
- Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it.
|
||||
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
|
||||
- Header ID generation has some minor changes to bring the ID generation closer to other tools and sites:
|
||||
- IDs now use Unicode lowercase instead of ASCII lowercase.
|
||||
[#2922](https://github.com/rust-lang/mdBook/pull/2922)
|
||||
- Headers that start or end with HTML characters like `<`, `&`, or `>` now replace those characters in the link ID with `-` instead of being stripped.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- Headers are no longer modified if the tag is manually written HTML.
|
||||
[#2913](https://github.com/rust-lang/mdBook/pull/2913)
|
||||
|
||||
### CLI changes
|
||||
|
||||
- Removed the `--dest-dir` option to `mdbook test`. It was unused since `mdbook test` does not generate output.
|
||||
[#2805](https://github.com/rust-lang/mdBook/pull/2805)
|
||||
- Changed CLI `--dest-dir` to be relative to the current directory, not the book root.
|
||||
[#2806](https://github.com/rust-lang/mdBook/pull/2806)
|
||||
|
||||
### Rust API
|
||||
|
||||
- The Rust API has been split into several crates ([#2766](https://github.com/rust-lang/mdBook/pull/2766)). In summary, the different crates are:
|
||||
- `mdbook` — The CLI binary.
|
||||
- [`mdbook-driver`](https://docs.rs/mdbook-driver/latest/mdbook_driver/) — The high-level library for running mdBook, primarily through the `MDBook` type. If you are driving mdBook programmatically, this is the crate you want.
|
||||
- [`mdbook-preprocessor`](https://docs.rs/mdbook-preprocessor/latest/mdbook_preprocessor/) — Support for implementing preprocessors. If you have a preprocessor, then this is the crate you should depend on.
|
||||
- [`mdbook-renderer`](https://docs.rs/mdbook-renderer/latest/mdbook_renderer/) — Support for implementing renderers. If you have a custom renderer, this is the crate you should depend on.
|
||||
- [`mdbook-markdown`](https://docs.rs/mdbook-markdown/latest/mdbook_markdown/) — The Markdown renderer. If you are processing markdown, this is the crate you should depend on. This is essentially a thin wrapper around `pulldown-cmark`, and re-exports that crate so that you can ensure the version stays in sync with mdBook.
|
||||
- [`mdbook-summary`](https://docs.rs/mdbook-summary/latest/mdbook_summary/) — The `SUMMARY.md` parser.
|
||||
- [`mdbook-html`](https://docs.rs/mdbook-html/latest/mdbook_html/) — The HTML renderer.
|
||||
- [`mdbook-core`](https://docs.rs/mdbook-core/latest/mdbook_core/) — An internal library that is used by the other crates for shared types. You should not depend on this crate directly since types from this crate are re-exported from the other crates as appropriate.
|
||||
- Changes to `Config`:
|
||||
- [`Config::get`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.get) is now generic over the return value, using `serde` to deserialize the value. It also returns a `Result` to handle deserialization errors. [#2773](https://github.com/rust-lang/mdBook/pull/2773)
|
||||
- [`Config::set`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.set) now validates that the config keys and values are valid.
|
||||
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
|
||||
- [`Config::update_from_env`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.update_from_env) now returns a `Result` to indicate any errors.
|
||||
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
|
||||
- Removed `Config::get_deserialized`. Use `Config::get` instead.
|
||||
- Removed `Config::get_deserialized_opt`. Use `Config::get` instead.
|
||||
- Removed `Config::get_mut`. Use `Config::set` instead.
|
||||
- Removed deprecated `Config::get_deserialized_opt`. Use `Config::get` instead.
|
||||
- Removed `Config::get_renderer`. Use `Config::get` instead.
|
||||
- Removed `Config::get_preprocessor`. Use `Config::get` instead.
|
||||
- Public types have been switch to use the `#[non_exhaustive]` attribute to help allow them to change in a backwards-compatible way.
|
||||
[#2779](https://github.com/rust-lang/mdBook/pull/2779)
|
||||
[#2823](https://github.com/rust-lang/mdBook/pull/2823)
|
||||
- Changed `MDBook` `with_renderer`/`with_preprocessor` to overwrite the entry if an extension of the same name is already loaded. This allows the caller to replace an entry.
|
||||
[#2802](https://github.com/rust-lang/mdBook/pull/2802)
|
||||
- Added `MarkdownOptions` struct to specify settings for markdown rendering for `mdbook_markdown::new_cmark_parser`.
|
||||
[#2809](https://github.cocm/rust-lang/mdBook/pull/2809)
|
||||
- Renamed `Book::sections` to `Book::items`.
|
||||
[#2813](https://github.com/rust-lang/mdBook/pull/2813)
|
||||
- `mdbook::book::load_book` is now private. Instead, use one of the `MDBook` load functions like `MDBook::load_with_config`.
|
||||
- Removed `HtmlConfig::smart_punctuation` method, use the field of the same name.
|
||||
- `CmdPreprocessor::parse_input` moved to `mdbook_preprocessor::parse_input`.
|
||||
- `Preprocessor::supports_renderer` now returns a `Result<bool>` instead of `bool` to be able to handle errors.
|
||||
- Most of the types from the `theme` module are now private. The `Theme` struct is still exposed for working with themes.
|
||||
- Various functions in the `utils::fs` module have been removed, renamed, or reworked.
|
||||
- Most of the functions in the `utils` module have been moved, removed, or made private.
|
||||
|
||||
## mdBook 0.5.0-beta.2
|
||||
[v0.5.0-beta.1...v0.5.0-beta.2](https://github.com/rust-lang/mdBook/compare/v0.5.0-beta.1...v0.5.0-beta.2)
|
||||
|
||||
### Added
|
||||
|
||||
- Added a warning when a Font Awesome icon is missing.
|
||||
[#2915](https://github.com/rust-lang/mdBook/pull/2915)
|
||||
- Added some trace logging for event processing.
|
||||
[#2911](https://github.com/rust-lang/mdBook/pull/2911)
|
||||
- Added `Config::contains_key`.
|
||||
[#2910](https://github.com/rust-lang/mdBook/pull/2910)
|
||||
|
||||
### Changed
|
||||
|
||||
- Heading IDs are now lowercase.
|
||||
[#2922](https://github.com/rust-lang/mdBook/pull/2922)
|
||||
- Updated cargo dependencies.
|
||||
[#2916](https://github.com/rust-lang/mdBook/pull/2916)
|
||||
- Removed italics for in quotes/comments in code blocks with the `ayu` theme.
|
||||
[#2904](https://github.com/rust-lang/mdBook/pull/2904)
|
||||
- Exposed "search" feature from mdbook-driver.
|
||||
[#2907](https://github.com/rust-lang/mdBook/pull/2907)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed rust fenced code blocks with an indent.
|
||||
[#2905](https://github.com/rust-lang/mdBook/pull/2905)
|
||||
- Headers and `dt` tags are no longer modified if the tag is manually written HTML.
|
||||
[#2913](https://github.com/rust-lang/mdBook/pull/2913)
|
||||
- Fixed print page links for internal links to non-chapters.
|
||||
[#2914](https://github.com/rust-lang/mdBook/pull/2914)
|
||||
- Better handling for unbalanced HTML tags.
|
||||
[#2924](https://github.com/rust-lang/mdBook/pull/2924)
|
||||
- Handle unclosed HTML tags inside a markdown element.
|
||||
[#2927](https://github.com/rust-lang/mdBook/pull/2927)
|
||||
- Fixed missing font-awesome icons in the guide.
|
||||
[#2926](https://github.com/rust-lang/mdBook/pull/2926)
|
||||
- Hide the sidebar resize indicator when JS isn't available.
|
||||
[#2923](https://github.com/rust-lang/mdBook/pull/2923)
|
||||
|
||||
## mdBook 0.5.0-beta.1
|
||||
[v0.5.0-alpha.1...v0.5.0-beta.1](https://github.com/rust-lang/mdBook/compare/v0.5.0-alpha.1...v0.5.0-beta.1)
|
||||
|
||||
### Changed
|
||||
|
||||
- Reworked the look of the header navigation.
|
||||
[#2898](https://github.com/rust-lang/mdBook/pull/2898)
|
||||
- Update cargo dependencies.
|
||||
[#2896](https://github.com/rust-lang/mdBook/pull/2896)
|
||||
- Improved the heading nav debug.
|
||||
[#2892](https://github.com/rust-lang/mdBook/pull/2892)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed error message for config.get deserialization error.
|
||||
[#2902](https://github.com/rust-lang/mdBook/pull/2902)
|
||||
- Filter `<mark>` tags from sidebar heading nav.
|
||||
[#2899](https://github.com/rust-lang/mdBook/pull/2899)
|
||||
- Avoid divide-by-zero in heading nav computation
|
||||
[#2891](https://github.com/rust-lang/mdBook/pull/2891)
|
||||
- Fixed heading nav with folded chapters.
|
||||
[#2893](https://github.com/rust-lang/mdBook/pull/2893)
|
||||
|
||||
## mdBook 0.5.0-alpha.1
|
||||
[v0.4.52...v0.5.0-alpha.1](https://github.com/rust-lang/mdBook/compare/v0.4.52...v0.5.0-alpha.1)
|
||||
|
||||
### Added
|
||||
|
||||
- The location of the generated HTML book is now displayed on the console.
|
||||
[#2729](https://github.com/rust-lang/mdBook/pull/2729)
|
||||
- ❗ Added the `optional` field for preprocessors. The default is `false`, so this also changes it so that it is an error if the preprocessor is missing.
|
||||
[#2797](https://github.com/rust-lang/mdBook/pull/2797)
|
||||
- ❗ Added `MarkdownOptions` struct to specify settings for markdown rendering.
|
||||
[#2809](https://github.cocm/rust-lang/mdBook/pull/2809)
|
||||
- Added sidebar heading navigation. This includes the `output.html.sidebar-header-nav` option to disable it.
|
||||
[#2822](https://github.com/rust-lang/mdBook/pull/2822)
|
||||
- Added the mdbook version to the guide.
|
||||
[#2826](https://github.com/rust-lang/mdBook/pull/2826)
|
||||
- Added `Book::chapters` and `Book::for_each_chapter_mut` to more conveniently iterate over chapters (instead of all items).
|
||||
[#2838](https://github.com/rust-lang/mdBook/pull/2838)
|
||||
- ❗ Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it.
|
||||
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
|
||||
- ❗ Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it.
|
||||
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
|
||||
|
||||
### Changed
|
||||
|
||||
- ❗ The `mdbook` crate has been split into multiple crates.
|
||||
[#2766](https://github.com/rust-lang/mdBook/pull/2766)
|
||||
- The minimum Rust version has been updated to 1.88.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- ❗ `pulldown-cmark` has been upgraded to 0.13.0, bringing a large number of fixes to markdown processing.
|
||||
[#2401](https://github.com/rust-lang/mdBook/pull/2401)
|
||||
- ❗ Switched public types to `non_exhaustive` to help allow them to change in a backwards-compatible way.
|
||||
[#2779](https://github.com/rust-lang/mdBook/pull/2779)
|
||||
[#2823](https://github.com/rust-lang/mdBook/pull/2823)
|
||||
- ❗ Unknown fields in config are now an error.
|
||||
[#2787](https://github.com/rust-lang/mdBook/pull/2787)
|
||||
[#2801](https://github.com/rust-lang/mdBook/pull/2801)
|
||||
- ❗ Changed `id_from_content` to be private.
|
||||
[#2791](https://github.com/rust-lang/mdBook/pull/2791)
|
||||
- ❗ Changed preprocessor `command` to use paths relative to the book root.
|
||||
[#2796](https://github.com/rust-lang/mdBook/pull/2796)
|
||||
- ❗ Replaced the `{{#previous}}` and `{{#next}}` handelbars navigation helpers with objects.
|
||||
[#2794](https://github.com/rust-lang/mdBook/pull/2794)
|
||||
- ❗ Use embedded SVG instead of fonts for icons, font-awesome 6.2.
|
||||
[#1330](https://github.com/rust-lang/mdBook/pull/1330)
|
||||
- The `book.src` field is no longer serialized if it is the default of "src".
|
||||
[#2800](https://github.com/rust-lang/mdBook/pull/2800)
|
||||
- ❗ Changed `MDBook` `with_renderer`/`with_preprocessor` to overwrite the entry if an extension of the same name is already loaded.
|
||||
[#2802](https://github.com/rust-lang/mdBook/pull/2802)
|
||||
- ❗ Changed CLI `--dest-dir` to be relative to the current directory, not the book root.
|
||||
[#2806](https://github.com/rust-lang/mdBook/pull/2806)
|
||||
- ❗ Changed all internal HTML IDs to have an `mdbook-` prefix. This helps avoid namespace conflicts with header IDs.
|
||||
[#2808](https://github.com/rust-lang/mdBook/pull/2808)
|
||||
- ❗ `output.html.smart-punctuation` is now `true` by default.
|
||||
[#2810](https://github.com/rust-lang/mdBook/pull/2810)
|
||||
- ❗ Renamed `Book::sections` to `Book::items`.
|
||||
[#2813](https://github.com/rust-lang/mdBook/pull/2813)
|
||||
- ❗ `output.html.hash-files` is now `true` by default.
|
||||
[#2820](https://github.com/rust-lang/mdBook/pull/2820)
|
||||
- Switched from `log` to `tracing`.
|
||||
[#2829](https://github.com/rust-lang/mdBook/pull/2829)
|
||||
- ❗ Rewrote the HTML rendering pipeline.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- ❗ Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- ❗ Moved theme copy to the Theme type and reduced visibility.
|
||||
[#2857](https://github.com/rust-lang/mdBook/pull/2857)
|
||||
- ❗ Cleaned up some fs-related utilities.
|
||||
[#2856](https://github.com/rust-lang/mdBook/pull/2856)
|
||||
- ❗ Moved `get_404_output_file` to `HtmlConfig`.
|
||||
[#2855](https://github.com/rust-lang/mdBook/pull/2855)
|
||||
- ❗ Moved `take_lines` functions to `mdbook-driver` and made private.
|
||||
[#2854](https://github.com/rust-lang/mdBook/pull/2854)
|
||||
- Updated dependencies.
|
||||
[#2793](https://github.com/rust-lang/mdBook/pull/2793)
|
||||
[#2869](https://github.com/rust-lang/mdBook/pull/2869)
|
||||
|
||||
### Removed
|
||||
|
||||
- ❗ Removed `toml` as a public dependency.
|
||||
[#2773](https://github.com/rust-lang/mdBook/pull/2773)
|
||||
- ❗ Removed the `book.multilingual` field. This was never used.
|
||||
[#2775](https://github.com/rust-lang/mdBook/pull/2775)
|
||||
- ❗ Removed support for google-analytics.
|
||||
[#2776](https://github.com/rust-lang/mdBook/pull/2776)
|
||||
- ❗ Removed the very old legacy config support.
|
||||
[#2783](https://github.com/rust-lang/mdBook/pull/2783)
|
||||
- ❗ Removed `curly-quotes`, use `output.html.smart-punctuation` instead.
|
||||
[#2788](https://github.com/rust-lang/mdBook/pull/2788)
|
||||
- Removed old warning about `book.json`.
|
||||
[#2789](https://github.com/rust-lang/mdBook/pull/2789)
|
||||
- ❗ Removed `output.html.copy-fonts`. The default fonts are now always copied unless you override the `theme/fonts/fonts.css` file.
|
||||
[#2790](https://github.com/rust-lang/mdBook/pull/2790)
|
||||
- ❗ Removed legacy relative renderer command paths. Relative renderer command paths now must always be relative to the book root.
|
||||
[#2792](https://github.com/rust-lang/mdBook/pull/2792)
|
||||
- ❗ Removed the `{{theme_option}}` handlebars helper. It has not been used for a while.
|
||||
[#2795](https://github.com/rust-lang/mdBook/pull/2795)
|
||||
- ❗ Removed the `--dest-dir` option to `mdbook test`.
|
||||
[#2805](https://github.com/rust-lang/mdBook/pull/2805)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed handling of multiple footnotes in a row.
|
||||
[#2807](https://github.com/rust-lang/mdBook/pull/2807)
|
||||
- Fixed ID collisions when the numeric suffix gets used.
|
||||
[#2846](https://github.com/rust-lang/mdBook/pull/2846)
|
||||
- Fixed missing css vars for no-js dark mode.
|
||||
[#2850](https://github.com/rust-lang/mdBook/pull/2850)
|
||||
|
||||
## mdBook 0.4.52
|
||||
[v0.4.51...v0.4.52](https://github.com/rust-lang/mdBook/compare/v0.4.51...v0.4.52)
|
||||
|
||||
|
||||
@@ -56,11 +56,11 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdbook` or `mdbook.exe`.
|
||||
|
||||
## Code Quality
|
||||
## Code quality
|
||||
|
||||
We love code quality and Rust has some excellent tools to assist you with contributions.
|
||||
|
||||
### Formatting Code with rustfmt
|
||||
### Formatting code with rustfmt
|
||||
|
||||
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
|
||||
This will ensure we have good quality source code that is better for us all to maintain.
|
||||
@@ -84,7 +84,7 @@ The quick guide is
|
||||
|
||||
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
|
||||
### Finding issues with 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.
|
||||
@@ -123,6 +123,33 @@ Please consider the following when making a change:
|
||||
|
||||
* Check out the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) for guidelines on designing the API.
|
||||
|
||||
## Tests
|
||||
|
||||
The main test harness is described in the [testsuite documentation](tests/testsuite/README.md). There are several different commands to run different kinds of tests:
|
||||
|
||||
- `cargo test --workspace` — This runs all of the unit and integration tests, except for the GUI tests.
|
||||
- `cargo test --test gui` — This runs the [GUI test harness](#browser-compatibility-and-testing). This does not get run automatically due to its extra requirements.
|
||||
- `npm run lint` — [Checks the `.js` files](#checking-changes-in-js-files)
|
||||
- `cargo test --workspace --no-default-features` — Testing without default features helps check that all feature checks are implemented correctly.
|
||||
- `cargo clippy --workspace --all-targets --no-deps -- -D warnings` — This makes sure that there are no clippy warnings.
|
||||
- `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --document-private-items --no-deps` — This verifies that there aren't any rustdoc warnings.
|
||||
- `cargo fmt --check` — Verifies that everything is formatted correctly.
|
||||
- `cargo +stable semver-checks` — Verifies that no SemVer breaking changes have been made. You must install [`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks) first.
|
||||
|
||||
To help simplify running all these commands, you can run the following cargo command:
|
||||
|
||||
```sh
|
||||
cargo xtask test-all
|
||||
```
|
||||
|
||||
It is useful to run all tests before submitting a PR. While developing I recommend to run some subset of that command based on what you are working on. There are individual arguments for each one. For example:
|
||||
|
||||
```sh
|
||||
cargo xtask test-workspace clippy doc eslint fmt gui semver-checks
|
||||
```
|
||||
|
||||
While developing, remove any of those arguments that are not relevant to what you are changing, or are really slow.
|
||||
|
||||
## Making a pull-request
|
||||
|
||||
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||
@@ -158,9 +185,7 @@ If you want to disable the headless mode, use the `--disable-headless-test` opti
|
||||
cargo test --test gui -- --disable-headless-test
|
||||
```
|
||||
|
||||
The GUI tests are in the directory `tests/gui` in text files with the `.goml` extension. These tests are run
|
||||
using a `node.js` framework called `browser-ui-test`. You can find documentation for this language on its
|
||||
[repository](https://github.com/GuillaumeGomez/browser-UI-test/blob/master/goml-script.md).
|
||||
The GUI tests are in the directory `tests/gui` in text files with the `.goml` extension. The books that the tests use are located in the `tests/gui/books` directory. These tests are run using a `node.js` framework called `browser-ui-test`. You can find documentation for this language on its [repository](https://github.com/GuillaumeGomez/browser-UI-test/blob/master/goml-script.md).
|
||||
|
||||
### Checking changes in `.js` files
|
||||
|
||||
@@ -188,20 +213,24 @@ The following are instructions for updating [highlight.js](https://highlightjs.o
|
||||
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.
|
||||
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 [syntax GUI test](https://github.com/rust-lang/mdBook/tree/master/tests/gui/books/highlighting) contains a chapter with many languages to examine. Update the test (`highlighting.goml`) to add any new languages.
|
||||
|
||||
## 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.
|
||||
1. Create a PR that bumps the version and updates the changelog:
|
||||
1. `git fetch upstream`
|
||||
2. `git checkout -B bump-version upstream/master && git branch --set-upstream-to=origin/bump-version`
|
||||
3. `cargo xtask bump <BUMP>`
|
||||
- This will update the version of all the crates.
|
||||
- `cargo set-version` must first be installed with `cargo install cargo-edit`.
|
||||
- Replace `<BUMP>` with the kind of bump (patch, alpha, etc.)
|
||||
4. `cargo xtask changelog`
|
||||
- This will update `CHANGELOG.md` to add a list of all changes at the top. You will need to move those into the appropriate categories. Most changes that are generally not relevant to a user should be removed. Rewrite the descriptions so that a user can reasonably figure out what it means.
|
||||
5. `git add --update .`
|
||||
6. `git commit`
|
||||
7. `git push`
|
||||
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`" ; \
|
||||
|
||||
1712
Cargo.lock
generated
1712
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
146
Cargo.toml
146
Cargo.toml
@@ -1,80 +1,136 @@
|
||||
[workspace]
|
||||
members = [".", "examples/remove-emphasis/mdbook-remove-emphasis"]
|
||||
members = [
|
||||
".",
|
||||
"crates/*",
|
||||
"examples/remove-emphasis/mdbook-remove-emphasis", "guide/guide-helper",
|
||||
]
|
||||
|
||||
[workspace.lints.clippy]
|
||||
all = { level = "allow", priority = -2 }
|
||||
correctness = { level = "warn", priority = -1 }
|
||||
complexity = { level = "warn", priority = -1 }
|
||||
needless-lifetimes = "allow" # Remove once 1.87 is stable, https://github.com/rust-lang/rust-clippy/issues/13514
|
||||
exhaustive_enums = "warn"
|
||||
exhaustive_structs = "warn"
|
||||
manual_non_exhaustive = "warn"
|
||||
|
||||
[workspace.lints.rust]
|
||||
missing_docs = "warn"
|
||||
rust_2018_idioms = "warn"
|
||||
unreachable_pub = "warn"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
rust-version = "1.88.0" # Keep in sync with installation.md and .github/workflows/main.yml
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
axum = "0.8.7"
|
||||
clap = { version = "4.5.53", features = ["cargo", "wrap_help"] }
|
||||
clap_complete = "4.5.61"
|
||||
ego-tree = "0.10.0"
|
||||
elasticlunr-rs = "3.0.2"
|
||||
font-awesome-as-a-crate = "0.3.0"
|
||||
futures-util = "0.3.31"
|
||||
glob = "0.3.3"
|
||||
handlebars = "6.3.2"
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.36.0"
|
||||
indexmap = "2.12.1"
|
||||
ignore = "0.4.25"
|
||||
mdbook-core = { path = "crates/mdbook-core", version = "0.5.2" }
|
||||
mdbook-driver = { path = "crates/mdbook-driver", version = "0.5.2" }
|
||||
mdbook-html = { path = "crates/mdbook-html", version = "0.5.2" }
|
||||
mdbook-markdown = { path = "crates/mdbook-markdown", version = "0.5.2" }
|
||||
mdbook-preprocessor = { path = "crates/mdbook-preprocessor", version = "0.5.2" }
|
||||
mdbook-renderer = { path = "crates/mdbook-renderer", version = "0.5.2" }
|
||||
mdbook-summary = { path = "crates/mdbook-summary", version = "0.5.2" }
|
||||
memchr = "2.7.6"
|
||||
notify = "8.2.0"
|
||||
notify-debouncer-mini = "0.7.0"
|
||||
opener = "0.8.3"
|
||||
pathdiff = "0.2.3"
|
||||
pulldown-cmark = { version = "0.13.0", default-features = false, features = ["html"] } # Do not update, part of the public api.
|
||||
regex = "1.12.2"
|
||||
select = "0.6.1"
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
sha2 = "0.10.9"
|
||||
shlex = "1.3.0"
|
||||
snapbox = "0.6.23"
|
||||
tempfile = "3.23.0"
|
||||
tokio = "1.48.0"
|
||||
toml = "0.9.8"
|
||||
topological-sort = "0.2.2"
|
||||
tower-http = "0.6.7"
|
||||
tracing = "0.1.43"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
walkdir = "2.5.0"
|
||||
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.52"
|
||||
version = "0.5.2"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
"Matt Ickstadt <mattico8@gmail.com>"
|
||||
]
|
||||
documentation = "https://rust-lang.github.io/mdBook/index.html"
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
exclude = ["/guide/*"]
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
license.workspace = true
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
repository.workspace = true
|
||||
description = "Creates a book from markdown files"
|
||||
rust-version = "1.82" # Keep in sync with installation.md and .github/workflows/main.yml
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
env_logger = "0.11.1"
|
||||
handlebars = "6.0"
|
||||
hex = "0.4.3"
|
||||
log = "0.4.17"
|
||||
memchr = "2.5.0"
|
||||
opener = "0.8.1"
|
||||
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"
|
||||
sha2 = "0.10.8"
|
||||
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"
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
clap_complete.workspace = true
|
||||
mdbook-core.workspace = true
|
||||
mdbook-driver.workspace = true
|
||||
mdbook-html.workspace = true
|
||||
mdbook-markdown.workspace = true
|
||||
mdbook-preprocessor.workspace = true
|
||||
mdbook-renderer.workspace = true
|
||||
mdbook-summary.workspace = true
|
||||
opener.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "8.0.0", optional = true }
|
||||
notify-debouncer-mini = { version = "0.6.0", optional = true }
|
||||
ignore = { version = "0.4.20", optional = true }
|
||||
pathdiff = { version = "0.2.1", optional = true }
|
||||
walkdir = { version = "2.3.3", optional = true }
|
||||
ignore = { workspace = true, optional = true }
|
||||
notify = { workspace = true, optional = true }
|
||||
notify-debouncer-mini = { workspace = true, optional = true }
|
||||
pathdiff = { workspace = true, optional = true }
|
||||
walkdir = { workspace = true, optional = true }
|
||||
|
||||
# Serve feature
|
||||
futures-util = { version = "0.3.28", optional = true }
|
||||
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
axum = { version = "0.8.0", features = ["ws"], optional = true }
|
||||
tower-http = { version = "0.6.0", features = ["fs", "trace"], optional = true }
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "3.0.2", optional = true }
|
||||
ammonia = { version = "4.0.0", optional = true }
|
||||
axum = { workspace = true, features = ["ws"], optional = true }
|
||||
futures-util = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"], optional = true }
|
||||
tower-http = { workspace = true, features = ["fs", "trace"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
select = "0.6.0"
|
||||
semver = "1.0.17"
|
||||
snapbox = { version = "0.6.21", features = ["diff", "dir", "term-svg", "regex", "json"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
walkdir = "2.3.3"
|
||||
glob.workspace = true
|
||||
regex.workspace = true
|
||||
select.workspace = true
|
||||
semver.workspace = true
|
||||
serde_json.workspace = true
|
||||
snapbox = { workspace = true, features = ["diff", "dir", "term-svg", "regex", "json"] }
|
||||
tempfile.workspace = true
|
||||
walkdir.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["watch", "serve", "search"]
|
||||
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"]
|
||||
serve = ["dep:futures-util", "dep:tokio", "dep:axum", "dep:tower-http"]
|
||||
search = ["dep:elasticlunr-rs", "dep:ammonia"]
|
||||
search = ["mdbook-html/search"]
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
|
||||
38
ci/publish-guide.sh
Executable file
38
ci/publish-guide.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# This publishes the user guide to GitHub Pages.
|
||||
#
|
||||
# If this is a pre-release, then it goes in a separate directory called "pre-release".
|
||||
# Commits are amended to avoid keeping history which can balloon the repo size.
|
||||
set -ex
|
||||
|
||||
cargo run --no-default-features -F search -- build guide
|
||||
|
||||
VERSION=$(cargo metadata --format-version 1 --no-deps | jq '.packages[] | select(.name == "mdbook") | .version')
|
||||
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
PRERELEASE=true
|
||||
else
|
||||
PRERELEASE=false
|
||||
fi
|
||||
|
||||
git fetch origin gh-pages
|
||||
git worktree add gh-pages gh-pages
|
||||
git config user.name "Deploy from CI"
|
||||
git config user.email ""
|
||||
cd gh-pages
|
||||
if [[ "$PRERELEASE" == "true" ]]
|
||||
then
|
||||
rm -rf pre-release
|
||||
mv ../guide/book pre-release
|
||||
git add pre-release
|
||||
git commit --amend -m "Deploy $GITHUB_SHA pre-release to gh-pages"
|
||||
else
|
||||
# Delete everything except pre-release and .git.
|
||||
find . -mindepth 1 -maxdepth 1 -not -name "pre-release" -not -name ".git" -exec rm -rf {} +
|
||||
# Copy the guide here.
|
||||
find ../guide/book/ -mindepth 1 -maxdepth 1 -exec mv {} . \;
|
||||
git add .
|
||||
git commit --amend -m "Deploy $GITHUB_SHA to gh-pages"
|
||||
fi
|
||||
|
||||
git push --force origin +gh-pages
|
||||
44
ci/update-dependencies.sh
Executable file
44
ci/update-dependencies.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Updates all compatible Cargo dependencies.
|
||||
#
|
||||
# I wasn't able to get Renovate to update compatible dependencies in a way
|
||||
# that I like, so this script takes care of it. This uses `cargo upgrade` to
|
||||
# ensure that `Cargo.toml` also gets updated. This also makes sure that all
|
||||
# transitive dependencies are updated.
|
||||
|
||||
set -ex
|
||||
|
||||
git fetch origin update-dependencies
|
||||
if git checkout update-dependencies
|
||||
then
|
||||
git reset --hard origin/master
|
||||
else
|
||||
git checkout -b update-dependencies
|
||||
fi
|
||||
|
||||
cat > commit-message << 'EOF'
|
||||
Update cargo dependencies
|
||||
|
||||
```
|
||||
EOF
|
||||
cargo upgrade >> commit-message
|
||||
echo '```' >> commit-message
|
||||
if git diff --quiet
|
||||
then
|
||||
echo "No changes detected, exiting."
|
||||
exit 0
|
||||
fi
|
||||
# Also update any transitive dependencies.
|
||||
cargo update
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add Cargo.toml Cargo.lock
|
||||
git commit -F commit-message
|
||||
|
||||
git push --force origin update-dependencies
|
||||
|
||||
gh pr create --fill \
|
||||
--head update-dependencies \
|
||||
--base master
|
||||
12
crates/mdbook-compare/Cargo.toml
Normal file
12
crates/mdbook-compare/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "mdbook-compare"
|
||||
publish = false
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
26
crates/mdbook-compare/README.md
Normal file
26
crates/mdbook-compare/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# mdbook-compare
|
||||
|
||||
This is a simple utility to compare the output of two different versions of mdbook.
|
||||
|
||||
To use this:
|
||||
|
||||
1. Install [`tidy`](https://www.html-tidy.org/).
|
||||
2. Install or build the initial version of mdbook that you want to compare.
|
||||
3. Install or build the new version of mdbook that you want to compare.
|
||||
4. Run `mdbook-compare` with the arguments to the mdbook executables and the books to build.
|
||||
|
||||
```sh
|
||||
cargo run --manifest-path /path/to/mdBook/Cargo.toml -p mdbook-compare -- \
|
||||
/path/to/orig/mdbook /path/to/my-book /path/to/new/mdbook /path/to/my-book
|
||||
```
|
||||
|
||||
It takes two separate paths for the book to use for "before" and "after" in case you need to customize the book to run on older versions. If you don't need that, then you can use the same directory for both the before and after.
|
||||
|
||||
`mdbook-compare` will do the following:
|
||||
|
||||
1. Clean up any book directories.
|
||||
2. Build the book with the first mdbook.
|
||||
3. Build the book with the second mdbook.
|
||||
4. The output of those two commands are stored in directories called `compare1` and `compare2`.
|
||||
5. The HTML in those directories is normalized using `tidy`.
|
||||
6. Runs `git diff` to compare the output.
|
||||
113
crates/mdbook-compare/src/main.rs
Normal file
113
crates/mdbook-compare/src/main.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
//! Utility to compare the output of two different versions of mdbook.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
macro_rules! error {
|
||||
($msg:literal $($arg:tt)*) => {
|
||||
eprint!("error: ");
|
||||
eprintln!($msg $($arg)*);
|
||||
std::process::exit(1);
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let (Some(mdbook1), Some(book1), Some(mdbook2), Some(book2)) =
|
||||
(args.next(), args.next(), args.next(), args.next())
|
||||
else {
|
||||
eprintln!("error: Expected four arguments: <exe1> <dir1> <exe2> <dir2>");
|
||||
std::process::exit(1);
|
||||
};
|
||||
let mdbook1 = Path::new(&mdbook1);
|
||||
let mdbook2 = Path::new(&mdbook2);
|
||||
let book1 = Path::new(&book1);
|
||||
let book2 = Path::new(&book2);
|
||||
let compare1 = Path::new("compare1");
|
||||
let compare2 = Path::new("compare2");
|
||||
clean(compare1);
|
||||
clean(compare2);
|
||||
clean(&book1.join("book"));
|
||||
clean(&book2.join("book"));
|
||||
build(mdbook1, book1);
|
||||
std::fs::rename(book1.join("book"), compare1).unwrap();
|
||||
build(mdbook2, book2);
|
||||
std::fs::rename(book2.join("book"), compare2).unwrap();
|
||||
diff(compare1, compare2);
|
||||
}
|
||||
|
||||
fn clean(path: &Path) {
|
||||
if path.exists() {
|
||||
println!("removing {path:?}");
|
||||
std::fs::remove_dir_all(path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn build(mdbook: &Path, book: &Path) {
|
||||
println!("running `{mdbook:?} build` in `{book:?}`");
|
||||
let status = Command::new(mdbook)
|
||||
.arg("build")
|
||||
.current_dir(book)
|
||||
.status()
|
||||
.unwrap_or_else(|e| {
|
||||
error!("expected {mdbook:?} executable to exist: {e}");
|
||||
});
|
||||
if !status.success() {
|
||||
error!("process {mdbook:?} failed");
|
||||
}
|
||||
process(&book.join("book"));
|
||||
}
|
||||
|
||||
fn process(path: &Path) {
|
||||
for entry in std::fs::read_dir(path).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
process(&path);
|
||||
} else {
|
||||
if path.extension().is_some_and(|ext| ext == "html") {
|
||||
tidy(&path);
|
||||
process_html(&path);
|
||||
} else {
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_html(path: &Path) {
|
||||
let content = std::fs::read_to_string(path).unwrap();
|
||||
let Some(start_index) = content.find("<main>") else {
|
||||
return;
|
||||
};
|
||||
let end_index = content.rfind("</main>").unwrap();
|
||||
let new_content = &content[start_index..end_index + 8];
|
||||
std::fs::write(path, new_content).unwrap();
|
||||
}
|
||||
|
||||
fn tidy(path: &Path) {
|
||||
// quiet, no wrap, modify in place
|
||||
let args = "-q -w 0 -m --custom-tags yes --drop-empty-elements no";
|
||||
println!("running `tidy {args}` in `{path:?}`");
|
||||
let status = Command::new("tidy")
|
||||
.args(args.split(' '))
|
||||
.arg(path)
|
||||
.status()
|
||||
.expect("tidy should be installed");
|
||||
if !status.success() {
|
||||
// Exit code 1 is a warning.
|
||||
if status.code() != Some(1) {
|
||||
error!("tidy failed: {status}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn diff(a: &Path, b: &Path) {
|
||||
let args = "diff --no-index";
|
||||
println!("running `git {args} {a:?} {b:?}`");
|
||||
Command::new("git")
|
||||
.args(args.split(' '))
|
||||
.args([a, b])
|
||||
.status()
|
||||
.unwrap();
|
||||
}
|
||||
22
crates/mdbook-core/Cargo.toml
Normal file
22
crates/mdbook-core/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "mdbook-core"
|
||||
version = "0.5.2"
|
||||
description = "The base support library for mdbook, intended for internal use only"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
13
crates/mdbook-core/README.md
Normal file
13
crates/mdbook-core/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# mdbook-core
|
||||
|
||||
[](https://docs.rs/mdbook-core)
|
||||
[](https://crates.io/crates/mdbook-core)
|
||||
[](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
|
||||
|
||||
This is the base support library for [mdBook](https://rust-lang.github.io/mdBook/). It is intended for internal use only. Other mdBook crates depend on this for any types that are shared across the crates.
|
||||
|
||||
> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning.
|
||||
|
||||
## License
|
||||
|
||||
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)
|
||||
288
crates/mdbook-core/src/book.rs
Normal file
288
crates/mdbook-core/src/book.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
//! A tree structure representing a book.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// A tree structure representing a book.
|
||||
///
|
||||
/// A book is just a collection of [`BookItems`] which are accessible by
|
||||
/// either iterating (immutably) over the book with [`iter()`], or recursively
|
||||
/// applying a closure to each item to mutate the chapters, using
|
||||
/// [`for_each_mut()`].
|
||||
///
|
||||
/// [`iter()`]: #method.iter
|
||||
/// [`for_each_mut()`]: #method.for_each_mut
|
||||
#[allow(
|
||||
clippy::exhaustive_structs,
|
||||
reason = "This cannot be extended without breaking preprocessors."
|
||||
)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Book {
|
||||
/// The items in this book.
|
||||
pub items: Vec<BookItem>,
|
||||
}
|
||||
|
||||
impl Book {
|
||||
/// Create an empty book.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Creates a new book with the given items.
|
||||
pub fn new_with_items(items: Vec<BookItem>) -> Book {
|
||||
Book { items }
|
||||
}
|
||||
|
||||
/// Get a depth-first iterator over the items in the book.
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
BookItems {
|
||||
items: self.items.iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A depth-first iterator over each [`Chapter`], skipping draft chapters.
|
||||
pub fn chapters(&self) -> impl Iterator<Item = &Chapter> {
|
||||
self.iter().filter_map(|item| match item {
|
||||
BookItem::Chapter(ch) if !ch.is_draft_chapter() => Some(ch),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Recursively apply a closure to each item in the book, allowing you to
|
||||
/// mutate them.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Unlike the `iter()` method, this requires a closure instead of returning
|
||||
/// an iterator. This is because using iterators can possibly allow you
|
||||
/// to have iterator invalidation errors.
|
||||
pub fn for_each_mut<F>(&mut self, mut func: F)
|
||||
where
|
||||
F: FnMut(&mut BookItem),
|
||||
{
|
||||
for_each_mut(&mut func, &mut self.items);
|
||||
}
|
||||
|
||||
/// Recursively apply a closure to each non-draft chapter in the book,
|
||||
/// allowing you to mutate them.
|
||||
pub fn for_each_chapter_mut<F>(&mut self, mut func: F)
|
||||
where
|
||||
F: FnMut(&mut Chapter),
|
||||
{
|
||||
for_each_mut(
|
||||
&mut |item| {
|
||||
let BookItem::Chapter(ch) = item else {
|
||||
return;
|
||||
};
|
||||
if ch.is_draft_chapter() {
|
||||
return;
|
||||
}
|
||||
func(ch)
|
||||
},
|
||||
&mut self.items,
|
||||
);
|
||||
}
|
||||
|
||||
/// Append a `BookItem` to the `Book`.
|
||||
pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
|
||||
self.items.push(item.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn for_each_mut<'a, F, I>(func: &mut F, items: I)
|
||||
where
|
||||
F: FnMut(&mut BookItem),
|
||||
I: IntoIterator<Item = &'a mut BookItem>,
|
||||
{
|
||||
for item in items {
|
||||
if let BookItem::Chapter(ch) = item {
|
||||
for_each_mut(func, &mut ch.sub_items);
|
||||
}
|
||||
|
||||
func(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum representing any type of item which can be added to a book.
|
||||
#[allow(
|
||||
clippy::exhaustive_enums,
|
||||
reason = "This cannot be extended without breaking preprocessors."
|
||||
)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum BookItem {
|
||||
/// A nested chapter.
|
||||
Chapter(Chapter),
|
||||
/// A section separator.
|
||||
Separator,
|
||||
/// A part title.
|
||||
PartTitle(String),
|
||||
}
|
||||
|
||||
impl From<Chapter> for BookItem {
|
||||
fn from(other: Chapter) -> BookItem {
|
||||
BookItem::Chapter(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// The representation of a "chapter", usually mapping to a single file on
|
||||
/// disk however it may contain multiple sub-chapters.
|
||||
#[allow(
|
||||
clippy::exhaustive_structs,
|
||||
reason = "This cannot be extended without breaking preprocessors."
|
||||
)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Chapter {
|
||||
/// The chapter's name.
|
||||
pub name: String,
|
||||
/// The chapter's contents.
|
||||
pub content: String,
|
||||
/// The chapter's section number, if it has one.
|
||||
pub number: Option<SectionNumber>,
|
||||
/// Nested items.
|
||||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
///
|
||||
/// **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, or a synthetically generated
|
||||
/// chapter that has no file on disk.
|
||||
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>,
|
||||
}
|
||||
|
||||
impl Chapter {
|
||||
/// Create a new chapter with the provided content.
|
||||
pub fn new<P: Into<PathBuf>>(
|
||||
name: &str,
|
||||
content: String,
|
||||
p: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Chapter {
|
||||
let path: PathBuf = p.into();
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content,
|
||||
path: Some(path.clone()),
|
||||
source_path: Some(path),
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new draft chapter that is not attached to a source markdown file (and thus
|
||||
/// has no content).
|
||||
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content: String::new(),
|
||||
path: None,
|
||||
source_path: None,
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
|
||||
pub fn is_draft_chapter(&self) -> bool {
|
||||
self.path.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Chapter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ref section_number) = self.number {
|
||||
write!(f, "{section_number} ")?;
|
||||
}
|
||||
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
||||
/// a pretty `Display` impl.
|
||||
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SectionNumber(Vec<u32>);
|
||||
|
||||
impl SectionNumber {
|
||||
/// Creates a new [`SectionNumber`].
|
||||
pub fn new(numbers: impl Into<Vec<u32>>) -> SectionNumber {
|
||||
SectionNumber(numbers.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SectionNumber {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if self.0.is_empty() {
|
||||
write!(f, "0")
|
||||
} else {
|
||||
for item in &self.0 {
|
||||
write!(f, "{item}.")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SectionNumber {
|
||||
type Target = Vec<u32>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for SectionNumber {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<u32> for SectionNumber {
|
||||
fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
|
||||
SectionNumber(it.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// A depth-first iterator over the items in a book.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This struct shouldn't be created directly, instead prefer the
|
||||
/// [`Book::iter()`] method.
|
||||
pub struct BookItems<'a> {
|
||||
items: VecDeque<&'a BookItem>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BookItems<'a> {
|
||||
type Item = &'a BookItem;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let item = self.items.pop_front();
|
||||
|
||||
if let Some(BookItem::Chapter(ch)) = item {
|
||||
// if we wanted a breadth-first iterator we'd `extend()` here
|
||||
for sub_item in ch.sub_items.iter().rev() {
|
||||
self.items.push_front(sub_item);
|
||||
}
|
||||
}
|
||||
|
||||
item
|
||||
}
|
||||
}
|
||||
123
crates/mdbook-core/src/book/tests.rs
Normal file
123
crates/mdbook-core/src/book/tests.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn section_number_has_correct_dotted_representation() {
|
||||
let inputs = vec![
|
||||
(vec![0], "0."),
|
||||
(vec![1, 3], "1.3."),
|
||||
(vec![1, 2, 3], "1.2.3."),
|
||||
];
|
||||
|
||||
for (input, should_be) in inputs {
|
||||
let section_number = SectionNumber(input).to_string();
|
||||
assert_eq!(section_number, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn book_iter_iterates_over_sequential_items() {
|
||||
let items = vec![
|
||||
BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from("# Chapter 1"),
|
||||
..Default::default()
|
||||
}),
|
||||
BookItem::Separator,
|
||||
];
|
||||
let book = Book::new_with_items(items);
|
||||
|
||||
let should_be: Vec<_> = book.items.iter().collect();
|
||||
|
||||
let got: Vec<_> = book.iter().collect();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_each_mut_visits_all_items() {
|
||||
let items = vec![
|
||||
BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from("# Chapter 1"),
|
||||
number: None,
|
||||
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
source_path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Hello World",
|
||||
String::new(),
|
||||
"Chapter_1/hello.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Goodbye World",
|
||||
String::new(),
|
||||
"Chapter_1/goodbye.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
],
|
||||
}),
|
||||
BookItem::Separator,
|
||||
];
|
||||
let mut book = Book::new_with_items(items);
|
||||
|
||||
let num_items = book.iter().count();
|
||||
let mut visited = 0;
|
||||
|
||||
book.for_each_mut(|_| visited += 1);
|
||||
|
||||
assert_eq!(visited, num_items);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterate_over_nested_book_items() {
|
||||
let items = vec![
|
||||
BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from("# Chapter 1"),
|
||||
number: None,
|
||||
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
source_path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Hello World",
|
||||
String::new(),
|
||||
"Chapter_1/hello.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Goodbye World",
|
||||
String::new(),
|
||||
"Chapter_1/goodbye.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
],
|
||||
}),
|
||||
BookItem::Separator,
|
||||
];
|
||||
let book = Book::new_with_items(items);
|
||||
|
||||
let got: Vec<_> = book.iter().collect();
|
||||
|
||||
assert_eq!(got.len(), 5);
|
||||
|
||||
// checking the chapter names are in the order should be sufficient here...
|
||||
let chapter_names: Vec<String> = got
|
||||
.into_iter()
|
||||
.filter_map(|i| match *i {
|
||||
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let should_be: Vec<_> = vec![
|
||||
String::from("Chapter 1"),
|
||||
String::from("Hello World"),
|
||||
String::from("Goodbye World"),
|
||||
];
|
||||
|
||||
assert_eq!(chapter_names, should_be);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
16
crates/mdbook-core/src/lib.rs
Normal file
16
crates/mdbook-core/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
//! The base support library for mdbook, intended for internal use only.
|
||||
|
||||
/// The current version of `mdbook`.
|
||||
///
|
||||
/// This is provided as a way for custom preprocessors and renderers to do
|
||||
/// compatibility checks.
|
||||
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub mod book;
|
||||
pub mod config;
|
||||
pub mod utils;
|
||||
|
||||
/// The error types used in mdbook.
|
||||
pub mod errors {
|
||||
pub use anyhow::{Error, Result};
|
||||
}
|
||||
@@ -1,24 +1,38 @@
|
||||
//! Filesystem utilities and helpers.
|
||||
|
||||
use crate::errors::*;
|
||||
use log::{debug, trace};
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use tracing::debug;
|
||||
|
||||
/// Naively replaces any path separator with a forward-slash '/'
|
||||
pub fn normalize_path(path: &str) -> String {
|
||||
use std::path::is_separator;
|
||||
path.chars()
|
||||
.map(|ch| if is_separator(ch) { '/' } else { ch })
|
||||
.collect::<String>()
|
||||
/// Reads a file into a string.
|
||||
///
|
||||
/// Equivalent to [`std::fs::read_to_string`] with better error messages.
|
||||
pub fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let path = path.as_ref();
|
||||
fs::read_to_string(path).with_context(|| format!("failed to read `{}`", path.display()))
|
||||
}
|
||||
|
||||
/// Write the given data to a file, creating it first if necessary
|
||||
pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8]) -> Result<()> {
|
||||
let path = build_dir.join(filename);
|
||||
/// Writes a file to disk.
|
||||
///
|
||||
/// Equivalent to [`std::fs::write`] with better error messages. This will
|
||||
/// also create the parent directory if it doesn't exist.
|
||||
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
debug!("Writing `{}`", path.display());
|
||||
if let Some(parent) = path.parent() {
|
||||
create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(path, contents.as_ref())
|
||||
.with_context(|| format!("failed to write `{}`", path.display()))
|
||||
}
|
||||
|
||||
create_file(&path)?.write_all(content).map_err(Into::into)
|
||||
/// Equivalent to [`std::fs::create_dir_all`] with better error messages.
|
||||
pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
|
||||
let p = p.as_ref();
|
||||
fs::create_dir_all(p)
|
||||
.with_context(|| format!("failed to create directory `{}`", p.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes a path and returns a path containing just enough `../` to point to
|
||||
@@ -29,7 +43,7 @@ pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8])
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::path::Path;
|
||||
/// # use mdbook::utils::fs::path_to_root;
|
||||
/// # use mdbook_core::utils::fs::path_to_root;
|
||||
/// let path = Path::new("some/relative/path");
|
||||
/// assert_eq!(path_to_root(path), "../../");
|
||||
/// ```
|
||||
@@ -56,30 +70,19 @@ pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
})
|
||||
}
|
||||
|
||||
/// This function creates a file and returns it. But before creating the file
|
||||
/// it checks every directory in the path to see if it exists,
|
||||
/// and if it does not it will be created.
|
||||
pub fn create_file(path: &Path) -> Result<File> {
|
||||
debug!("Creating {}", path.display());
|
||||
|
||||
// Construct path
|
||||
if let Some(p) = path.parent() {
|
||||
trace!("Parent directory is: {:?}", p);
|
||||
|
||||
fs::create_dir_all(p)?;
|
||||
}
|
||||
|
||||
File::create(path).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Removes all the content of a directory but not the directory itself
|
||||
/// 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)?.flatten() {
|
||||
for item in fs::read_dir(dir)
|
||||
.with_context(|| format!("failed to read directory `{}`", dir.display()))?
|
||||
.flatten()
|
||||
{
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
fs::remove_dir_all(item)?;
|
||||
fs::remove_dir_all(&item)
|
||||
.with_context(|| format!("failed to remove `{}`", item.display()))?;
|
||||
} else {
|
||||
fs::remove_file(item)?;
|
||||
fs::remove_file(&item)
|
||||
.with_context(|| format!("failed to remove `{}`", item.display()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -170,7 +173,7 @@ fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
|
||||
use std::fs::OpenOptions;
|
||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
|
||||
let mut reader = File::open(from)?;
|
||||
let mut reader = std::fs::File::open(from)?;
|
||||
let metadata = reader.metadata()?;
|
||||
if !metadata.is_file() {
|
||||
anyhow::bail!(
|
||||
@@ -204,18 +207,11 @@ fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the name of the file used for HTTP 404 "not found" with the `.html` extension.
|
||||
pub fn get_404_output_file(input_404: &Option<String>) -> String {
|
||||
input_404
|
||||
.as_ref()
|
||||
.unwrap_or(&"404.md".to_string())
|
||||
.replace(".md", ".html")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::copy_files_except_ext;
|
||||
use std::{fs, io::Result, path::Path};
|
||||
use super::*;
|
||||
use std::io::Result;
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
|
||||
@@ -235,38 +231,18 @@ mod tests {
|
||||
};
|
||||
|
||||
// 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.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::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::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}");
|
||||
}
|
||||
write(tmp.path().join("file.txt"), "").unwrap();
|
||||
write(tmp.path().join("file.md"), "").unwrap();
|
||||
write(tmp.path().join("file.png"), "").unwrap();
|
||||
write(tmp.path().join("sub_dir/file.png"), "").unwrap();
|
||||
write(tmp.path().join("sub_dir_exists/file.txt"), "").unwrap();
|
||||
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/sub_dir_exists")) {
|
||||
panic!("Could not create output/sub_dir_exists: {err}");
|
||||
}
|
||||
create_dir_all(tmp.path().join("output")).unwrap();
|
||||
create_dir_all(tmp.path().join("output/sub_dir_exists")).unwrap();
|
||||
|
||||
if let Err(e) =
|
||||
copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])
|
||||
78
crates/mdbook-core/src/utils/html.rs
Normal file
78
crates/mdbook-core/src/utils/html.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
//! Utilities for dealing with HTML.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Escape characters to make it safe for an HTML string.
|
||||
pub fn escape_html_attribute(text: &str) -> Cow<'_, str> {
|
||||
let needs_escape: &[char] = &['<', '>', '\'', '"', '\\', '&'];
|
||||
let mut s = text;
|
||||
let mut output = String::new();
|
||||
while let Some(next) = s.find(needs_escape) {
|
||||
output.push_str(&s[..next]);
|
||||
match s.as_bytes()[next] {
|
||||
b'<' => output.push_str("<"),
|
||||
b'>' => output.push_str(">"),
|
||||
b'\'' => output.push_str("'"),
|
||||
b'"' => output.push_str("""),
|
||||
b'\\' => output.push_str("\"),
|
||||
b'&' => output.push_str("&"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
s = &s[next + 1..];
|
||||
}
|
||||
if output.is_empty() {
|
||||
Cow::Borrowed(text)
|
||||
} else {
|
||||
output.push_str(s);
|
||||
Cow::Owned(output)
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape `<`, `>`, and '&' for HTML.
|
||||
pub fn escape_html(text: &str) -> Cow<'_, str> {
|
||||
let needs_escape: &[char] = &['<', '>', '&'];
|
||||
let mut s = text;
|
||||
let mut output = String::new();
|
||||
while let Some(next) = s.find(needs_escape) {
|
||||
output.push_str(&s[..next]);
|
||||
match s.as_bytes()[next] {
|
||||
b'<' => output.push_str("<"),
|
||||
b'>' => output.push_str(">"),
|
||||
b'&' => output.push_str("&"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
s = &s[next + 1..];
|
||||
}
|
||||
if output.is_empty() {
|
||||
Cow::Borrowed(text)
|
||||
} else {
|
||||
output.push_str(s);
|
||||
Cow::Owned(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_are_escaped() {
|
||||
assert_eq!(escape_html_attribute(""), "");
|
||||
assert_eq!(escape_html_attribute("<"), "<");
|
||||
assert_eq!(escape_html_attribute(">"), ">");
|
||||
assert_eq!(escape_html_attribute("<>"), "<>");
|
||||
assert_eq!(escape_html_attribute("<test>"), "<test>");
|
||||
assert_eq!(escape_html_attribute("a<test>b"), "a<test>b");
|
||||
assert_eq!(escape_html_attribute("'"), "'");
|
||||
assert_eq!(escape_html_attribute("\\"), "\");
|
||||
assert_eq!(escape_html_attribute("&"), "&");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_is_escaped() {
|
||||
assert_eq!(escape_html(""), "");
|
||||
assert_eq!(escape_html("<"), "<");
|
||||
assert_eq!(escape_html(">"), ">");
|
||||
assert_eq!(escape_html("&"), "&");
|
||||
assert_eq!(escape_html("<>"), "<>");
|
||||
assert_eq!(escape_html("<test>"), "<test>");
|
||||
assert_eq!(escape_html("a<test>b"), "a<test>b");
|
||||
assert_eq!(escape_html("'"), "'");
|
||||
assert_eq!(escape_html("\\"), "\\");
|
||||
}
|
||||
37
crates/mdbook-core/src/utils/mod.rs
Normal file
37
crates/mdbook-core/src/utils/mod.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! Various helpers and utilities.
|
||||
|
||||
use anyhow::Error;
|
||||
use std::fmt::Write;
|
||||
use tracing::error;
|
||||
|
||||
pub mod fs;
|
||||
mod html;
|
||||
mod toml_ext;
|
||||
|
||||
pub(crate) use self::toml_ext::TomlExt;
|
||||
|
||||
pub use self::html::{escape_html, escape_html_attribute};
|
||||
|
||||
/// Defines a `static` with a [`regex::Regex`].
|
||||
#[macro_export]
|
||||
macro_rules! static_regex {
|
||||
($name:ident, $regex:literal) => {
|
||||
static $name: std::sync::LazyLock<regex::Regex> =
|
||||
std::sync::LazyLock::new(|| regex::Regex::new($regex).unwrap());
|
||||
};
|
||||
($name:ident, bytes, $regex:literal) => {
|
||||
static $name: std::sync::LazyLock<regex::bytes::Regex> =
|
||||
std::sync::LazyLock::new(|| regex::bytes::Regex::new($regex).unwrap());
|
||||
};
|
||||
}
|
||||
|
||||
/// Prints a "backtrace" of some `Error`.
|
||||
pub fn log_backtrace(e: &Error) {
|
||||
let mut message = format!("{e}");
|
||||
|
||||
for cause in e.chain().skip(1) {
|
||||
write!(message, "\n\tCaused by: {cause}").unwrap();
|
||||
}
|
||||
|
||||
error!("{message}");
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
//! Helper for working with toml types.
|
||||
|
||||
use toml::value::{Table, Value};
|
||||
|
||||
/// Helper for working with toml types.
|
||||
pub(crate) trait TomlExt {
|
||||
/// Read a dotted key.
|
||||
fn read(&self, key: &str) -> Option<&Value>;
|
||||
fn read_mut(&mut self, key: &str) -> Option<&mut Value>;
|
||||
/// Insert with a dotted key.
|
||||
fn insert(&mut self, key: &str, value: Value);
|
||||
fn delete(&mut self, key: &str) -> Option<Value>;
|
||||
}
|
||||
|
||||
impl TomlExt for Value {
|
||||
@@ -16,14 +19,6 @@ impl TomlExt for Value {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_mut(&mut self, key: &str) -> Option<&mut Value> {
|
||||
if let Some((head, tail)) = split(key) {
|
||||
self.get_mut(head)?.read_mut(tail)
|
||||
} else {
|
||||
self.get_mut(key)
|
||||
}
|
||||
}
|
||||
|
||||
fn insert(&mut self, key: &str, value: Value) {
|
||||
if !self.is_table() {
|
||||
*self = Value::Table(Table::new());
|
||||
@@ -40,16 +35,6 @@ impl TomlExt for Value {
|
||||
table.insert(key.to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&mut self, key: &str) -> Option<Value> {
|
||||
if let Some((head, tail)) = split(key) {
|
||||
self.get_mut(head)?.delete(tail)
|
||||
} else if let Some(table) = self.as_table_mut() {
|
||||
table.remove(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split(key: &str) -> Option<(&str, &str)> {
|
||||
@@ -65,12 +50,11 @@ fn split(key: &str) -> Option<(&str, &str)> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn read_simple_table() {
|
||||
let src = "[table]";
|
||||
let value = Value::from_str(src).unwrap();
|
||||
let value: Value = toml::from_str(src).unwrap();
|
||||
|
||||
let got = value.read("table").unwrap();
|
||||
|
||||
@@ -80,7 +64,7 @@ mod tests {
|
||||
#[test]
|
||||
fn read_nested_item() {
|
||||
let src = "[table]\nnested=true";
|
||||
let value = Value::from_str(src).unwrap();
|
||||
let value: Value = toml::from_str(src).unwrap();
|
||||
|
||||
let got = value.read("table.nested").unwrap();
|
||||
|
||||
@@ -107,24 +91,4 @@ mod tests {
|
||||
let inserted = value.read("first.second").unwrap();
|
||||
assert_eq!(inserted, &item);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_a_top_level_item() {
|
||||
let src = "top = true";
|
||||
let mut value = Value::from_str(src).unwrap();
|
||||
|
||||
let got = value.delete("top").unwrap();
|
||||
|
||||
assert_eq!(got, Value::Boolean(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_a_nested_item() {
|
||||
let src = "[table]\n nested = true";
|
||||
let mut value = Value::from_str(src).unwrap();
|
||||
|
||||
let got = value.delete("table.nested").unwrap();
|
||||
|
||||
assert_eq!(got, Value::Boolean(true));
|
||||
}
|
||||
}
|
||||
32
crates/mdbook-driver/Cargo.toml
Normal file
32
crates/mdbook-driver/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "mdbook-driver"
|
||||
version = "0.5.2"
|
||||
description = "High-level library for running mdBook"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
indexmap.workspace = true
|
||||
mdbook-core.workspace = true
|
||||
mdbook-html.workspace = true
|
||||
mdbook-markdown.workspace = true
|
||||
mdbook-preprocessor.workspace = true
|
||||
mdbook-renderer.workspace = true
|
||||
mdbook-summary.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
shlex.workspace = true
|
||||
tempfile.workspace = true
|
||||
toml.workspace = true
|
||||
topological-sort.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
search = ["mdbook-html/search"]
|
||||
13
crates/mdbook-driver/README.md
Normal file
13
crates/mdbook-driver/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# mdbook-driver
|
||||
|
||||
[](https://docs.rs/mdbook-driver)
|
||||
[](https://crates.io/crates/mdbook-driver)
|
||||
[](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
|
||||
|
||||
This is the high-level Rust library for running [mdBook](https://rust-lang.github.io/mdBook/). New books can be created using [`BookBuilder`](https://docs.rs/mdbook-driver/latest/mdbook_driver/init/struct.BookBuilder.html). The primary type [`MDBook`](https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html) can be used to manage and render books.
|
||||
|
||||
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
|
||||
|
||||
## License
|
||||
|
||||
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)
|
||||
@@ -1,50 +1,32 @@
|
||||
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};
|
||||
use anyhow::{Context, Result, ensure};
|
||||
use mdbook_core::book::Book;
|
||||
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Stdio};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
/// A custom preprocessor which will shell out to a 3rd-party program.
|
||||
///
|
||||
/// # Preprocessing Protocol
|
||||
///
|
||||
/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
|
||||
/// execute the shell command `$cmd supports $renderer`. If the renderer is
|
||||
/// supported, custom preprocessors should exit with a exit code of `0`,
|
||||
/// any other exit code be considered as unsupported.
|
||||
///
|
||||
/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
|
||||
/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
|
||||
/// should then "return" a processed book by printing it to `stdout` as JSON.
|
||||
/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
|
||||
/// to parse the input provided by `mdbook`.
|
||||
///
|
||||
/// Exiting with a non-zero exit code while preprocessing is considered an
|
||||
/// error. `stderr` is passed directly through to the user, so it can be used
|
||||
/// for logging or emitting warnings if desired.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// An example preprocessor is available in this project's `examples/`
|
||||
/// directory.
|
||||
/// See <https://rust-lang.github.io/mdBook/for_developers/preprocessors.html>
|
||||
/// for a description of the preprocessor protocol.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CmdPreprocessor {
|
||||
name: String,
|
||||
cmd: String,
|
||||
root: PathBuf,
|
||||
optional: bool,
|
||||
}
|
||||
|
||||
impl CmdPreprocessor {
|
||||
/// Create a new `CmdPreprocessor`.
|
||||
pub fn new(name: String, cmd: String) -> CmdPreprocessor {
|
||||
CmdPreprocessor { name, cmd }
|
||||
}
|
||||
|
||||
/// A convenience function custom preprocessors can use to parse the input
|
||||
/// written to `stdin` by a `CmdRenderer`.
|
||||
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
|
||||
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
|
||||
pub fn new(name: String, cmd: String, root: PathBuf, optional: bool) -> CmdPreprocessor {
|
||||
CmdPreprocessor {
|
||||
name,
|
||||
cmd,
|
||||
root,
|
||||
optional,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
|
||||
@@ -70,22 +52,6 @@ impl CmdPreprocessor {
|
||||
pub fn cmd(&self) -> &str {
|
||||
&self.cmd
|
||||
}
|
||||
|
||||
fn command(&self) -> Result<Command> {
|
||||
let mut words = Shlex::new(&self.cmd);
|
||||
let executable = match words.next() {
|
||||
Some(e) => e,
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(executable);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for CmdPreprocessor {
|
||||
@@ -94,19 +60,31 @@ impl Preprocessor for CmdPreprocessor {
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
|
||||
let mut cmd = self.command()?;
|
||||
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
|
||||
|
||||
let mut child = cmd
|
||||
let mut child = match cmd
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(&self.root)
|
||||
.spawn()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unable to start the \"{}\" preprocessor. Is it installed?",
|
||||
self.name()
|
||||
)
|
||||
})?;
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
crate::handle_command_error(
|
||||
e,
|
||||
self.optional,
|
||||
"preprocessor",
|
||||
"preprocessor",
|
||||
&self.name,
|
||||
&self.cmd,
|
||||
)?;
|
||||
// This should normally not be reached, since the validation
|
||||
// for NotFound should have already happened when running the
|
||||
// "supports" command.
|
||||
return Ok(book);
|
||||
}
|
||||
};
|
||||
|
||||
self.write_input_to_child(&mut child, &book, ctx);
|
||||
|
||||
@@ -134,45 +112,37 @@ impl Preprocessor for CmdPreprocessor {
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
fn supports_renderer(&self, renderer: &str) -> Result<bool> {
|
||||
debug!(
|
||||
"Checking if the \"{}\" preprocessor supports \"{}\"",
|
||||
self.name(),
|
||||
renderer
|
||||
);
|
||||
|
||||
let mut cmd = match self.command() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Unable to create the command for the \"{}\" preprocessor, {}",
|
||||
self.name(),
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let mut cmd = crate::compose_command(&self.cmd, &self.root)?;
|
||||
|
||||
let outcome = cmd
|
||||
match cmd
|
||||
.arg("supports")
|
||||
.arg(renderer)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(&self.root)
|
||||
.status()
|
||||
.map(|status| status.code() == Some(0));
|
||||
|
||||
if let Err(ref e) = outcome {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
warn!(
|
||||
"The command wasn't found, is the \"{}\" preprocessor installed?",
|
||||
self.name
|
||||
);
|
||||
warn!("\tCommand: {}", self.cmd);
|
||||
{
|
||||
Ok(status) => Ok(status.code() == Some(0)),
|
||||
Err(e) => {
|
||||
crate::handle_command_error(
|
||||
e,
|
||||
self.optional,
|
||||
"preprocessor",
|
||||
"preprocessor",
|
||||
&self.name,
|
||||
&self.cmd,
|
||||
)?;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
outcome.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,14 +153,19 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
fn guide() -> MDBook {
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../guide");
|
||||
MDBook::load(example).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_write_and_parse_input() {
|
||||
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
|
||||
let md = guide();
|
||||
let cmd = CmdPreprocessor::new(
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
md.root.clone(),
|
||||
false,
|
||||
);
|
||||
let ctx = PreprocessorContext::new(
|
||||
md.root.clone(),
|
||||
md.config.clone(),
|
||||
@@ -200,7 +175,7 @@ mod tests {
|
||||
let mut buffer = Vec::new();
|
||||
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
|
||||
|
||||
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
|
||||
let (got_ctx, got_book) = mdbook_preprocessor::parse_input(buffer.as_slice()).unwrap();
|
||||
|
||||
assert_eq!(got_book, md.book);
|
||||
assert_eq!(got_ctx, ctx);
|
||||
@@ -1,18 +1,19 @@
|
||||
use regex::Regex;
|
||||
use std::{path::Path, sync::LazyLock};
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::errors::*;
|
||||
use log::warn;
|
||||
use anyhow::Result;
|
||||
use mdbook_core::book::{Book, BookItem};
|
||||
use mdbook_core::static_regex;
|
||||
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
||||
use std::path::Path;
|
||||
use tracing::warn;
|
||||
|
||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||
/// `README.md` is the de facto index file in markdown-based documentation.
|
||||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct IndexPreprocessor;
|
||||
|
||||
impl IndexPreprocessor {
|
||||
pub(crate) const NAME: &'static str = "index";
|
||||
/// Name of this preprocessor.
|
||||
pub const NAME: &'static str = "index";
|
||||
|
||||
/// Create a new `IndexPreprocessor`.
|
||||
pub fn new() -> Self {
|
||||
@@ -67,9 +68,9 @@ fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
||||
}
|
||||
|
||||
fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)^readme$").unwrap());
|
||||
static_regex!(README, r"(?i)^readme$");
|
||||
|
||||
RE.is_match(
|
||||
README.is_match(
|
||||
path.as_ref()
|
||||
.file_stem()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
@@ -1,17 +1,18 @@
|
||||
use crate::errors::*;
|
||||
use crate::utils::{
|
||||
use self::take_lines::{
|
||||
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
||||
take_rustdoc_include_lines,
|
||||
};
|
||||
use regex::{CaptureMatches, Captures, Regex};
|
||||
use std::fs;
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook_core::book::{Book, BookItem};
|
||||
use mdbook_core::static_regex;
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
||||
use regex::{CaptureMatches, Captures};
|
||||
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use log::{error, warn};
|
||||
mod take_lines;
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
@@ -27,10 +28,12 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
/// - `{{# playground}}` - Insert runnable Rust files
|
||||
/// - `{{# title}}` - Override \<title\> of a webpage.
|
||||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct LinkPreprocessor;
|
||||
|
||||
impl LinkPreprocessor {
|
||||
pub(crate) const NAME: &'static str = "links";
|
||||
/// Name of this preprocessor.
|
||||
pub const NAME: &'static str = "links";
|
||||
|
||||
/// Create a new `LinkPreprocessor`.
|
||||
pub fn new() -> Self {
|
||||
@@ -407,23 +410,19 @@ impl<'a> Iterator for LinkIter<'a> {
|
||||
}
|
||||
|
||||
fn find_links(contents: &str) -> LinkIter<'_> {
|
||||
// lazily compute following regex
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?x) # insignificant whitespace mode
|
||||
static_regex!(
|
||||
LINK,
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9_]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([^}]+) # link target path and space separated properties
|
||||
\}\} # link closing parens",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
\}\} # link closing parens"
|
||||
);
|
||||
|
||||
LinkIter(RE.captures_iter(contents))
|
||||
LinkIter(LINK.captures_iter(contents))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -684,8 +683,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_playgrounds_with_properties() {
|
||||
let s =
|
||||
"Some random text with escaped playground {{#playground file.rs editable }} and some \
|
||||
let s = "Some random text with escaped playground {{#playground file.rs editable }} and some \
|
||||
more\n text {{#playground my.rs editable no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
@@ -714,8 +712,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_all_link_types() {
|
||||
let s =
|
||||
"Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
|
||||
let s = "Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
|
||||
insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
|
||||
no_run should_panic}} ...";
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use regex::Regex;
|
||||
use mdbook_core::static_regex;
|
||||
use std::ops::Bound::{Excluded, Included, Unbounded};
|
||||
use std::ops::RangeBounds;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Take a range of lines from a string.
|
||||
pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
pub(super) fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
let start = match range.start_bound() {
|
||||
Excluded(&n) => n + 1,
|
||||
Included(&n) => n,
|
||||
@@ -24,14 +23,12 @@ pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
static ANCHOR_START: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap());
|
||||
static ANCHOR_END: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap());
|
||||
static_regex!(ANCHOR_START, r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)");
|
||||
static_regex!(ANCHOR_END, r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)");
|
||||
|
||||
/// Take anchored lines from a string.
|
||||
/// Lines containing anchor are ignored.
|
||||
pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
pub(super) fn take_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
let mut retained = Vec::<&str>::new();
|
||||
let mut anchor_found = false;
|
||||
|
||||
@@ -63,7 +60,7 @@ pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
/// For any lines not in the range, include them but use `#` at the beginning. This will hide the
|
||||
/// lines from initial display but include them when expanding the code snippet or testing with
|
||||
/// rustdoc.
|
||||
pub fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
pub(super) fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
let mut output = String::with_capacity(s.len());
|
||||
|
||||
for (index, line) in s.lines().enumerate() {
|
||||
@@ -81,7 +78,7 @@ pub fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> S
|
||||
/// For any lines not between the anchors, include them but use `#` at the beginning. This will
|
||||
/// hide the lines from initial display but include them when expanding the code snippet or testing
|
||||
/// with rustdoc.
|
||||
pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
pub(super) fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
let mut output = String::with_capacity(s.len());
|
||||
let mut within_anchored_section = false;
|
||||
|
||||
9
crates/mdbook-driver/src/builtin_preprocessors/mod.rs
Normal file
9
crates/mdbook-driver/src/builtin_preprocessors/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! Built-in preprocessors.
|
||||
|
||||
pub use self::cmd::CmdPreprocessor;
|
||||
pub use self::index::IndexPreprocessor;
|
||||
pub use self::links::LinkPreprocessor;
|
||||
|
||||
mod cmd;
|
||||
mod index;
|
||||
mod links;
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::book::BookItem;
|
||||
use crate::errors::*;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
use log::trace;
|
||||
use std::fs;
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_renderer::{RenderContext, Renderer};
|
||||
use tracing::trace;
|
||||
|
||||
#[derive(Default)]
|
||||
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
|
||||
/// when debugging preprocessors.
|
||||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct MarkdownRenderer;
|
||||
|
||||
impl MarkdownRenderer {
|
||||
@@ -27,21 +26,16 @@ impl Renderer for MarkdownRenderer {
|
||||
let book = &ctx.book;
|
||||
|
||||
if destination.exists() {
|
||||
utils::fs::remove_dir_content(destination)
|
||||
fs::remove_dir_content(destination)
|
||||
.with_context(|| "Unable to remove stale Markdown output")?;
|
||||
}
|
||||
|
||||
trace!("markdown render");
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
if !ch.is_draft_chapter() {
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
ch.path.as_ref().expect("Checked path exists before"),
|
||||
ch.content.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
for ch in book.chapters() {
|
||||
let path = ctx
|
||||
.destination
|
||||
.join(ch.path.as_ref().expect("Checked path exists before"));
|
||||
fs::write(path, &ch.content)?;
|
||||
}
|
||||
|
||||
fs::create_dir_all(destination)
|
||||
88
crates/mdbook-driver/src/builtin_renderers/mod.rs
Normal file
88
crates/mdbook-driver/src/builtin_renderers/mod.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! Built-in renderers.
|
||||
//!
|
||||
//! The HTML renderer can be found in the [`mdbook_html`] crate.
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_renderer::{RenderContext, Renderer};
|
||||
use std::process::Stdio;
|
||||
use tracing::{error, info, trace, warn};
|
||||
|
||||
pub use self::markdown_renderer::MarkdownRenderer;
|
||||
|
||||
mod markdown_renderer;
|
||||
|
||||
/// A generic renderer which will shell out to an arbitrary executable.
|
||||
///
|
||||
/// See <https://rust-lang.github.io/mdBook/for_developers/backends.html>
|
||||
/// for a description of the renderer protocol.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CmdRenderer {
|
||||
name: String,
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
impl CmdRenderer {
|
||||
/// Create a new `CmdRenderer` which will invoke the provided `cmd` string.
|
||||
pub fn new(name: String, cmd: String) -> CmdRenderer {
|
||||
CmdRenderer { name, cmd }
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for CmdRenderer {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
info!("Invoking the \"{}\" renderer", self.name);
|
||||
|
||||
let optional_key = format!("output.{}.optional", self.name);
|
||||
let optional = match ctx.config.get(&optional_key) {
|
||||
Ok(Some(value)) => value,
|
||||
Err(e) => bail!("expected bool for `{optional_key}`: {e}"),
|
||||
Ok(None) => false,
|
||||
};
|
||||
|
||||
let _ = fs::create_dir_all(&ctx.destination);
|
||||
|
||||
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
|
||||
let mut child = match cmd
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(&ctx.destination)
|
||||
.spawn()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return crate::handle_command_error(
|
||||
e, optional, "output", "backend", &self.name, &self.cmd,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let mut stdin = child.stdin.take().expect("Child has stdin");
|
||||
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
|
||||
// Looks like the backend hung up before we could finish
|
||||
// sending it the render context. Log the error and keep going
|
||||
warn!("Error writing the RenderContext to the backend, {}", e);
|
||||
}
|
||||
|
||||
// explicitly close the `stdin` file handle
|
||||
drop(stdin);
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.with_context(|| "Error waiting for the backend to complete")?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, status);
|
||||
|
||||
if !status.success() {
|
||||
error!("Renderer exited with non-zero return code.");
|
||||
bail!("The \"{}\" renderer failed", self.name);
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
//! Support for initializing a new book.
|
||||
|
||||
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};
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook_core::config::Config;
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_html::theme::Theme;
|
||||
use std::path::PathBuf;
|
||||
use tracing::{debug, error, info, trace};
|
||||
|
||||
/// A helper for setting up a new book and its directory structure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -99,12 +98,10 @@ impl BookBuilder {
|
||||
fn write_book_toml(&self) -> Result<()> {
|
||||
debug!("Writing book.toml");
|
||||
let book_toml = self.root.join("book.toml");
|
||||
let cfg = toml::to_vec(&self.config).with_context(|| "Unable to serialize the config")?;
|
||||
let cfg =
|
||||
toml::to_string(&self.config).with_context(|| "Unable to serialize the config")?;
|
||||
|
||||
File::create(book_toml)
|
||||
.with_context(|| "Couldn't create book.toml")?
|
||||
.write_all(&cfg)
|
||||
.with_context(|| "Unable to write config to book.toml")?;
|
||||
fs::write(&book_toml, cfg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -112,76 +109,15 @@ impl BookBuilder {
|
||||
debug!("Copying theme");
|
||||
|
||||
let html_config = self.config.html_config().unwrap_or_default();
|
||||
let themedir = html_config.theme_dir(&self.root);
|
||||
|
||||
if !themedir.exists() {
|
||||
debug!(
|
||||
"{} does not exist, creating the directory",
|
||||
themedir.display()
|
||||
);
|
||||
fs::create_dir(&themedir)?;
|
||||
}
|
||||
|
||||
let mut index = File::create(themedir.join("index.hbs"))?;
|
||||
index.write_all(theme::INDEX)?;
|
||||
|
||||
let cssdir = themedir.join("css");
|
||||
if !cssdir.exists() {
|
||||
fs::create_dir(&cssdir)?;
|
||||
}
|
||||
|
||||
let mut general_css = File::create(cssdir.join("general.css"))?;
|
||||
general_css.write_all(theme::GENERAL_CSS)?;
|
||||
|
||||
let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
|
||||
chrome_css.write_all(theme::CHROME_CSS)?;
|
||||
|
||||
if html_config.print.enable {
|
||||
let mut print_css = File::create(cssdir.join("print.css"))?;
|
||||
print_css.write_all(theme::PRINT_CSS)?;
|
||||
}
|
||||
|
||||
let mut variables_css = File::create(cssdir.join("variables.css"))?;
|
||||
variables_css.write_all(theme::VARIABLES_CSS)?;
|
||||
|
||||
let mut favicon = File::create(themedir.join("favicon.png"))?;
|
||||
favicon.write_all(theme::FAVICON_PNG)?;
|
||||
|
||||
let mut favicon = File::create(themedir.join("favicon.svg"))?;
|
||||
favicon.write_all(theme::FAVICON_SVG)?;
|
||||
|
||||
let mut js = File::create(themedir.join("book.js"))?;
|
||||
js.write_all(theme::JS)?;
|
||||
|
||||
let mut highlight_css = File::create(themedir.join("highlight.css"))?;
|
||||
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
|
||||
|
||||
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
|
||||
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
|
||||
|
||||
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,
|
||||
)?;
|
||||
|
||||
Theme::copy_theme(&html_config, &self.root)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_gitignore(&self) -> Result<()> {
|
||||
debug!("Creating .gitignore");
|
||||
|
||||
let mut f = File::create(self.root.join(".gitignore"))?;
|
||||
|
||||
writeln!(f, "{}", self.config.build.build_dir.display())?;
|
||||
|
||||
fs::write(
|
||||
self.root.join(".gitignore"),
|
||||
format!("{}", self.config.build.build_dir.display()),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -192,14 +128,14 @@ impl BookBuilder {
|
||||
let summary = src_dir.join("SUMMARY.md");
|
||||
if !summary.exists() {
|
||||
trace!("No summary found creating stub summary and chapter_1.md.");
|
||||
let mut f = File::create(&summary).with_context(|| "Unable to create SUMMARY.md")?;
|
||||
writeln!(f, "# Summary")?;
|
||||
writeln!(f)?;
|
||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
fs::write(
|
||||
summary,
|
||||
"# Summary\n\
|
||||
\n\
|
||||
- [Chapter 1](./chapter_1.md)\n",
|
||||
)?;
|
||||
|
||||
let chapter_1 = src_dir.join("chapter_1.md");
|
||||
let mut f = File::create(chapter_1).with_context(|| "Unable to create chapter_1.md")?;
|
||||
writeln!(f, "# Chapter 1")?;
|
||||
fs::write(src_dir.join("chapter_1.md"), "# Chapter 1\n")?;
|
||||
} else {
|
||||
trace!("Existing summary found, no need to create stub files.");
|
||||
}
|
||||
131
crates/mdbook-driver/src/lib.rs
Normal file
131
crates/mdbook-driver/src/lib.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
//! High-level library for running mdBook.
|
||||
//!
|
||||
//! This is the high-level library for running
|
||||
//! [mdBook](https://rust-lang.github.io/mdBook/). There are several
|
||||
//! reasons for using the programmatic API (over the CLI):
|
||||
//!
|
||||
//! - Integrate mdBook in a current project.
|
||||
//! - Extend the capabilities of mdBook.
|
||||
//! - Do some processing or test before building your book.
|
||||
//! - Accessing the public API to help create a new Renderer.
|
||||
//!
|
||||
//! ## Additional crates
|
||||
//!
|
||||
//! In addition to `mdbook-driver`, there are several other crates available
|
||||
//! for using and extending mdBook:
|
||||
//!
|
||||
//! - [`mdbook_preprocessor`]: Provides support for implementing preprocessors.
|
||||
//! - [`mdbook_renderer`]: Provides support for implementing renderers.
|
||||
//! - [`mdbook_markdown`]: The Markdown renderer.
|
||||
//! - [`mdbook_summary`]: The `SUMMARY.md` parser.
|
||||
//! - [`mdbook_html`]: The HTML renderer.
|
||||
//! - [`mdbook_core`]: An internal library that is used by the other crates
|
||||
//! for shared types. Types from this crate are rexported from the other
|
||||
//! crates as appropriate.
|
||||
//!
|
||||
//! ## Cargo features
|
||||
//!
|
||||
//! The following cargo features are available:
|
||||
//!
|
||||
//! - `search`: Enables the search index in the HTML renderer.
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! If creating a new book from scratch, you'll want to get a [`init::BookBuilder`] via
|
||||
//! the [`MDBook::init()`] method.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use mdbook_driver::MDBook;
|
||||
//! use mdbook_driver::config::Config;
|
||||
//!
|
||||
//! let root_dir = "/path/to/book/root";
|
||||
//!
|
||||
//! // create a default config and change a couple things
|
||||
//! let mut cfg = Config::default();
|
||||
//! cfg.book.title = Some("My Book".to_string());
|
||||
//! cfg.book.authors.push("Michael-F-Bryan".to_string());
|
||||
//!
|
||||
//! MDBook::init(root_dir)
|
||||
//! .create_gitignore(true)
|
||||
//! .with_config(cfg)
|
||||
//! .build()
|
||||
//! .expect("Book generation failed");
|
||||
//! ```
|
||||
//!
|
||||
//! You can also load an existing book and build it.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use mdbook_driver::MDBook;
|
||||
//!
|
||||
//! let root_dir = "/path/to/book/root";
|
||||
//!
|
||||
//! let mut md = MDBook::load(root_dir)
|
||||
//! .expect("Unable to load the book");
|
||||
//! md.build().expect("Building failed");
|
||||
//! ```
|
||||
|
||||
pub mod builtin_preprocessors;
|
||||
pub mod builtin_renderers;
|
||||
pub mod init;
|
||||
mod load;
|
||||
mod mdbook;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
pub use mdbook::MDBook;
|
||||
pub use mdbook_core::{book, config, errors};
|
||||
use shlex::Shlex;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tracing::{error, warn};
|
||||
|
||||
/// Creates a [`Command`] for command renderers and preprocessors.
|
||||
fn compose_command(cmd: &str, root: &Path) -> Result<Command> {
|
||||
let mut words = Shlex::new(cmd);
|
||||
let exe = match words.next() {
|
||||
Some(e) => PathBuf::from(e),
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let exe = if exe.components().count() == 1 {
|
||||
// Search PATH for the executable.
|
||||
exe
|
||||
} else {
|
||||
// Relative path is relative to book root.
|
||||
root.join(&exe)
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(exe);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Handles a failure for a preprocessor or renderer.
|
||||
fn handle_command_error(
|
||||
error: std::io::Error,
|
||||
optional: bool,
|
||||
key: &str,
|
||||
what: &str,
|
||||
name: &str,
|
||||
cmd: &str,
|
||||
) -> Result<()> {
|
||||
if let std::io::ErrorKind::NotFound = error.kind() {
|
||||
if optional {
|
||||
warn!(
|
||||
"The command `{cmd}` for {what} `{name}` was not found, \
|
||||
but is marked as optional.",
|
||||
);
|
||||
return Ok(());
|
||||
} else {
|
||||
error!(
|
||||
"The command `{cmd}` wasn't found, is the `{name}` {what} installed? \
|
||||
If you want to ignore this error when the `{name}` {what} is not installed, \
|
||||
set `optional = true` in the `[{key}.{name}]` section of the book.toml configuration file.",
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error).with_context(|| format!("Unable to run the {what} `{name}`"))?
|
||||
}
|
||||
309
crates/mdbook-driver/src/load.rs
Normal file
309
crates/mdbook-driver/src/load.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook_core::book::{Book, BookItem, Chapter};
|
||||
use mdbook_core::config::BuildConfig;
|
||||
use mdbook_core::utils::{escape_html, fs};
|
||||
use mdbook_summary::{Link, Summary, SummaryItem, parse_summary};
|
||||
use std::path::Path;
|
||||
use tracing::debug;
|
||||
|
||||
/// Load a book into memory from its `src/` directory.
|
||||
pub(crate) fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let summary_md = src_dir.join("SUMMARY.md");
|
||||
|
||||
let summary_content = fs::read_to_string(&summary_md)?;
|
||||
let summary = parse_summary(&summary_content)
|
||||
.with_context(|| format!("Summary parsing failed for file={summary_md:?}"))?;
|
||||
|
||||
if cfg.create_missing {
|
||||
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
|
||||
}
|
||||
|
||||
load_book_from_disk(&summary, src_dir)
|
||||
}
|
||||
|
||||
fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||
let mut items: Vec<_> = summary
|
||||
.prefix_chapters
|
||||
.iter()
|
||||
.chain(summary.numbered_chapters.iter())
|
||||
.chain(summary.suffix_chapters.iter())
|
||||
.collect();
|
||||
|
||||
while 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);
|
||||
if !filename.exists() {
|
||||
if let Some(parent) = filename.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
debug!("Creating missing file {}", filename.display());
|
||||
let title = escape_html(&link.name);
|
||||
fs::write(&filename, format!("# {title}\n"))?;
|
||||
}
|
||||
}
|
||||
|
||||
items.extend(&link.nested_items);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Use the provided `Summary` to load a `Book` from disk.
|
||||
///
|
||||
/// You need to pass in the book's source directory because all the links in
|
||||
/// `SUMMARY.md` give the chapter locations relative to it.
|
||||
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||
debug!("Loading the book from disk");
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
let prefix = summary.prefix_chapters.iter();
|
||||
let numbered = summary.numbered_chapters.iter();
|
||||
let suffix = summary.suffix_chapters.iter();
|
||||
|
||||
let summary_items = prefix.chain(numbered).chain(suffix);
|
||||
|
||||
let mut chapters = Vec::new();
|
||||
|
||||
for summary_item in summary_items {
|
||||
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
|
||||
chapters.push(chapter);
|
||||
}
|
||||
|
||||
Ok(Book::new_with_items(chapters))
|
||||
}
|
||||
|
||||
fn load_summary_item<P: AsRef<Path> + Clone>(
|
||||
item: &SummaryItem,
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<BookItem> {
|
||||
match item {
|
||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||
SummaryItem::Link(link) => load_chapter(link, src_dir, parent_names).map(BookItem::Chapter),
|
||||
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
|
||||
_ => panic!("SummaryItem {item:?} not covered"),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_chapter<P: AsRef<Path>>(
|
||||
link: &Link,
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<Chapter> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
let mut ch = if let Some(ref link_location) = link.location {
|
||||
debug!("Loading {} ({})", link.name, link_location.display());
|
||||
|
||||
let location = if link_location.is_absolute() {
|
||||
link_location.clone()
|
||||
} else {
|
||||
src_dir.join(link_location)
|
||||
};
|
||||
|
||||
let mut content = std::fs::read_to_string(&location)
|
||||
.with_context(|| format!("failed to read chapter `{}`", link_location.display()))?;
|
||||
|
||||
if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
|
||||
content.replace_range(..3, "");
|
||||
}
|
||||
|
||||
let stripped = location
|
||||
.strip_prefix(src_dir)
|
||||
.expect("Chapters are always inside a book");
|
||||
|
||||
Chapter::new(&link.name, content, stripped, parent_names.clone())
|
||||
} else {
|
||||
Chapter::new_draft(&link.name, parent_names.clone())
|
||||
};
|
||||
|
||||
let mut sub_item_parents = parent_names;
|
||||
|
||||
ch.number = link.number.clone();
|
||||
|
||||
sub_item_parents.push(link.name.clone());
|
||||
let sub_items = link
|
||||
.nested_items
|
||||
.iter()
|
||||
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
ch.sub_items = sub_items;
|
||||
|
||||
Ok(ch)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mdbook_core::book::SectionNumber;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &str = "
|
||||
# Dummy Chapter
|
||||
|
||||
this is some dummy text.
|
||||
|
||||
And here is some \
|
||||
more text.
|
||||
";
|
||||
|
||||
/// Create a dummy `Link` in a temporary directory.
|
||||
fn dummy_link() -> (Link, TempDir) {
|
||||
let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||
|
||||
let chapter_path = temp.path().join("chapter_1.md");
|
||||
fs::write(&chapter_path, DUMMY_SRC).unwrap();
|
||||
|
||||
let link = Link::new("Chapter 1", chapter_path);
|
||||
|
||||
(link, temp)
|
||||
}
|
||||
|
||||
/// Create a nested `Link` written to a temporary directory.
|
||||
fn nested_links() -> (Link, TempDir) {
|
||||
let (mut root, temp_dir) = dummy_link();
|
||||
|
||||
let second_path = temp_dir.path().join("second.md");
|
||||
fs::write(&second_path, "Hello World!").unwrap();
|
||||
|
||||
let mut second = Link::new("Nested Chapter 1", &second_path);
|
||||
second.number = Some(SectionNumber::new([1, 2]));
|
||||
|
||||
root.nested_items.push(second.clone().into());
|
||||
root.nested_items.push(SummaryItem::Separator);
|
||||
root.nested_items.push(second.into());
|
||||
|
||||
(root, temp_dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_a_single_chapter_from_disk() {
|
||||
let (link, temp_dir) = dummy_link();
|
||||
let should_be = Chapter::new(
|
||||
"Chapter 1",
|
||||
DUMMY_SRC.to_string(),
|
||||
"chapter_1.md",
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_a_single_chapter_with_utf8_bom_from_disk() {
|
||||
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||
|
||||
let chapter_path = temp_dir.path().join("chapter_1.md");
|
||||
fs::write(&chapter_path, format!("\u{feff}{DUMMY_SRC}")).unwrap();
|
||||
|
||||
let link = Link::new("Chapter 1", chapter_path);
|
||||
|
||||
let should_be = Chapter::new(
|
||||
"Chapter 1",
|
||||
DUMMY_SRC.to_string(),
|
||||
"chapter_1.md",
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_load_a_nonexistent_chapter() {
|
||||
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
|
||||
|
||||
let got = load_chapter(&link, "", Vec::new());
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_recursive_link_with_separators() {
|
||||
let (root, temp) = nested_links();
|
||||
|
||||
let mut nested = Chapter::new(
|
||||
"Nested Chapter 1",
|
||||
String::from("Hello World!"),
|
||||
"second.md",
|
||||
vec![String::from("Chapter 1")],
|
||||
);
|
||||
nested.number = Some(SectionNumber::new([1, 2]));
|
||||
let mut chapter =
|
||||
Chapter::new("Chapter 1", String::from(DUMMY_SRC), "chapter_1.md", vec![]);
|
||||
chapter.sub_items = vec![
|
||||
BookItem::Chapter(nested.clone()),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(nested),
|
||||
];
|
||||
let should_be = BookItem::Chapter(chapter);
|
||||
|
||||
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_a_book_with_a_single_chapter() {
|
||||
let (link, temp) = dummy_link();
|
||||
let mut summary = Summary::default();
|
||||
summary.numbered_chapters = vec![SummaryItem::Link(link)];
|
||||
let chapter = Chapter::new(
|
||||
"Chapter 1",
|
||||
String::from(DUMMY_SRC),
|
||||
PathBuf::from("chapter_1.md"),
|
||||
vec![],
|
||||
);
|
||||
let items = vec![BookItem::Chapter(chapter)];
|
||||
let should_be = Book::new_with_items(items);
|
||||
|
||||
let got = load_book_from_disk(&summary, temp.path()).unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_load_chapters_with_an_empty_path() {
|
||||
let (_, temp) = dummy_link();
|
||||
let mut summary = Summary::default();
|
||||
let link = Link::new("Empty", "");
|
||||
summary.numbered_chapters = vec![SummaryItem::Link(link)];
|
||||
let got = load_book_from_disk(&summary, temp.path());
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_load_chapters_when_the_link_is_a_directory() {
|
||||
let (_, temp) = dummy_link();
|
||||
let dir = temp.path().join("nested");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
let mut summary = Summary::default();
|
||||
let link = Link::new("nested", dir);
|
||||
summary.numbered_chapters = vec![SummaryItem::Link(link)];
|
||||
|
||||
let got = load_book_from_disk(&summary, temp.path());
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_open_summary_md() {
|
||||
let cfg = BuildConfig::default();
|
||||
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||
|
||||
let got = load_book(&temp_dir, &cfg);
|
||||
assert!(got.is_err());
|
||||
let error_message = got.err().unwrap().to_string();
|
||||
let expected = format!(
|
||||
r#"failed to read `{}`"#,
|
||||
temp_dir.path().join("SUMMARY.md").display()
|
||||
);
|
||||
assert_eq!(error_message, expected);
|
||||
}
|
||||
}
|
||||
569
crates/mdbook-driver/src/mdbook.rs
Normal file
569
crates/mdbook-driver/src/mdbook.rs
Normal file
@@ -0,0 +1,569 @@
|
||||
//! The high-level interface for loading and rendering books.
|
||||
|
||||
use crate::builtin_preprocessors::{CmdPreprocessor, IndexPreprocessor, LinkPreprocessor};
|
||||
use crate::builtin_renderers::{CmdRenderer, MarkdownRenderer};
|
||||
use crate::init::BookBuilder;
|
||||
use crate::load::{load_book, load_book_from_disk};
|
||||
use anyhow::{Context, Error, Result, bail};
|
||||
use indexmap::IndexMap;
|
||||
use mdbook_core::book::{Book, BookItem, BookItems};
|
||||
use mdbook_core::config::{Config, RustEdition};
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_html::HtmlHandlebars;
|
||||
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
||||
use mdbook_renderer::{RenderContext, Renderer};
|
||||
use mdbook_summary::Summary;
|
||||
use serde::Deserialize;
|
||||
use std::ffi::OsString;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use topological_sort::TopologicalSort;
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// The object used to manage and build a book.
|
||||
pub struct MDBook {
|
||||
/// The book's root directory.
|
||||
pub root: PathBuf,
|
||||
|
||||
/// The configuration used to tweak now a book is built.
|
||||
pub config: Config,
|
||||
|
||||
/// A representation of the book's contents in memory.
|
||||
pub book: Book,
|
||||
|
||||
/// Renderers to execute.
|
||||
renderers: IndexMap<String, Box<dyn Renderer>>,
|
||||
|
||||
/// Pre-processors to be run on the book.
|
||||
preprocessors: IndexMap<String, Box<dyn Preprocessor>>,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
/// Load a book from its root directory on disk.
|
||||
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
|
||||
let book_root = book_root.into();
|
||||
let config_location = book_root.join("book.toml");
|
||||
|
||||
let mut config = if config_location.exists() {
|
||||
debug!("Loading config from {}", config_location.display());
|
||||
Config::from_disk(&config_location)?
|
||||
} else {
|
||||
Config::default()
|
||||
};
|
||||
|
||||
config.update_from_env()?;
|
||||
|
||||
if tracing::enabled!(tracing::Level::TRACE) {
|
||||
for line in format!("Config: {config:#?}").lines() {
|
||||
trace!("{}", line);
|
||||
}
|
||||
}
|
||||
|
||||
MDBook::load_with_config(book_root, config)
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom `Config`.
|
||||
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
|
||||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = load_book(src_dir, &config.build)?;
|
||||
|
||||
let renderers = determine_renderers(&config)?;
|
||||
let preprocessors = determine_preprocessors(&config, &root)?;
|
||||
|
||||
Ok(MDBook {
|
||||
root,
|
||||
config,
|
||||
book,
|
||||
renderers,
|
||||
preprocessors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom `Config` and a custom summary.
|
||||
pub fn load_with_config_and_summary<P: Into<PathBuf>>(
|
||||
book_root: P,
|
||||
config: Config,
|
||||
summary: Summary,
|
||||
) -> Result<MDBook> {
|
||||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = load_book_from_disk(&summary, src_dir)?;
|
||||
|
||||
let renderers = determine_renderers(&config)?;
|
||||
let preprocessors = determine_preprocessors(&config, &root)?;
|
||||
|
||||
Ok(MDBook {
|
||||
root,
|
||||
config,
|
||||
book,
|
||||
renderers,
|
||||
preprocessors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a flat depth-first iterator over the [`BookItem`]s of the book.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use mdbook_driver::MDBook;
|
||||
/// # use mdbook_driver::book::BookItem;
|
||||
/// # let book = MDBook::load("mybook").unwrap();
|
||||
/// for item in book.iter() {
|
||||
/// match *item {
|
||||
/// BookItem::Chapter(ref chapter) => {},
|
||||
/// BookItem::Separator => {},
|
||||
/// BookItem::PartTitle(ref title) => {}
|
||||
/// _ => {}
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // would print something like this:
|
||||
/// // 1. Chapter 1
|
||||
/// // 1.1 Sub Chapter
|
||||
/// // 1.2 Sub Chapter
|
||||
/// // 2. Chapter 2
|
||||
/// //
|
||||
/// // etc.
|
||||
/// ```
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
self.book.iter()
|
||||
}
|
||||
|
||||
/// `init()` gives you a `BookBuilder` which you can use to setup a new book
|
||||
/// and its accompanying directory structure.
|
||||
///
|
||||
/// The `BookBuilder` creates some boilerplate files and directories to get
|
||||
/// you started with your book.
|
||||
///
|
||||
/// ```text
|
||||
/// book-test/
|
||||
/// ├── book
|
||||
/// └── src
|
||||
/// ├── chapter_1.md
|
||||
/// └── SUMMARY.md
|
||||
/// ```
|
||||
///
|
||||
/// It uses the path provided as the root directory for your book, then adds
|
||||
/// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
|
||||
/// to get you started.
|
||||
pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
|
||||
BookBuilder::new(book_root)
|
||||
}
|
||||
|
||||
/// Tells the renderer to build our book and put it in the build directory.
|
||||
pub fn build(&self) -> Result<()> {
|
||||
info!("Book building has started");
|
||||
|
||||
for renderer in self.renderers.values() {
|
||||
self.execute_build_process(&**renderer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.values() {
|
||||
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);
|
||||
|
||||
let mut render_context = RenderContext::new(
|
||||
self.root.clone(),
|
||||
preprocessed_book,
|
||||
self.config.clone(),
|
||||
build_dir,
|
||||
);
|
||||
render_context
|
||||
.chapter_titles
|
||||
.extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
|
||||
|
||||
info!("Running the {} backend", renderer.name());
|
||||
renderer
|
||||
.render(&render_context)
|
||||
.with_context(|| "Rendering failed")
|
||||
}
|
||||
|
||||
/// You can change the default renderer to another one by using this method.
|
||||
/// The only requirement is that your renderer implement the [`Renderer`]
|
||||
/// trait.
|
||||
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
|
||||
self.renderers
|
||||
.insert(renderer.name().to_string(), Box::new(renderer));
|
||||
self
|
||||
}
|
||||
|
||||
/// Register a [`Preprocessor`] to be used when rendering the book.
|
||||
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
self.preprocessors
|
||||
.insert(preprocessor.name().to_string(), Box::new(preprocessor));
|
||||
self
|
||||
}
|
||||
|
||||
/// Run `rustdoc` tests on the book, linking against the provided libraries.
|
||||
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
||||
// 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()?;
|
||||
|
||||
let mut chapter_found = false;
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let chapter_path = match ch.path {
|
||||
Some(ref path) if !path.as_os_str().is_empty() => path,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
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);
|
||||
fs::write(&path, &ch.content)?;
|
||||
|
||||
let mut cmd = Command::new("rustdoc");
|
||||
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"]);
|
||||
}
|
||||
RustEdition::E2018 => {
|
||||
cmd.args(["--edition", "2018"]);
|
||||
}
|
||||
RustEdition::E2021 => {
|
||||
cmd.args(["--edition", "2021"]);
|
||||
}
|
||||
RustEdition::E2024 => {
|
||||
cmd.args(["--edition", "2024"]);
|
||||
}
|
||||
_ => panic!("RustEdition {edition:?} not covered"),
|
||||
}
|
||||
}
|
||||
|
||||
if color_output {
|
||||
cmd.args(["--color", "always"]);
|
||||
}
|
||||
|
||||
debug!("running {:?}", cmd);
|
||||
let output = cmd
|
||||
.output()
|
||||
.with_context(|| "failed to execute `rustdoc`")?;
|
||||
|
||||
if !output.status.success() {
|
||||
failed = true;
|
||||
eprintln!(
|
||||
"ERROR rustdoc returned an error:\n\
|
||||
\n--- stdout\n{}\n--- stderr\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
bail!("One or more tests failed");
|
||||
}
|
||||
if let Some(chapter) = chapter {
|
||||
if !chapter_found {
|
||||
bail!("Chapter not found: {}", chapter);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The logic for determining where a backend should put its build
|
||||
/// artefacts.
|
||||
///
|
||||
/// If there is only 1 renderer, put it in the directory pointed to by the
|
||||
/// `build.build_dir` key in [`Config`]. If there is more than one then the
|
||||
/// renderer gets its own directory within the main build dir.
|
||||
///
|
||||
/// i.e. If there were only one renderer (in this case, the HTML renderer):
|
||||
///
|
||||
/// - build/
|
||||
/// - index.html
|
||||
/// - ...
|
||||
///
|
||||
/// Otherwise if there are multiple:
|
||||
///
|
||||
/// - build/
|
||||
/// - epub/
|
||||
/// - my_awesome_book.epub
|
||||
/// - html/
|
||||
/// - index.html
|
||||
/// - ...
|
||||
/// - latex/
|
||||
/// - my_awesome_book.tex
|
||||
///
|
||||
pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
|
||||
let build_dir = self.root.join(&self.config.build.build_dir);
|
||||
|
||||
if self.renderers.len() <= 1 {
|
||||
build_dir
|
||||
} else {
|
||||
build_dir.join(backend_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the directory containing this book's source files.
|
||||
pub fn source_dir(&self) -> PathBuf {
|
||||
self.root.join(&self.config.book.src)
|
||||
}
|
||||
|
||||
/// Get the directory containing the theme resources for the book.
|
||||
pub fn theme_dir(&self) -> PathBuf {
|
||||
self.config
|
||||
.html_config()
|
||||
.unwrap_or_default()
|
||||
.theme_dir(&self.root)
|
||||
}
|
||||
}
|
||||
|
||||
/// An `output` table.
|
||||
#[derive(Deserialize)]
|
||||
struct OutputConfig {
|
||||
command: Option<String>,
|
||||
}
|
||||
|
||||
/// Look at the `Config` and try to figure out what renderers to use.
|
||||
fn determine_renderers(config: &Config) -> Result<IndexMap<String, Box<dyn Renderer>>> {
|
||||
let mut renderers = IndexMap::new();
|
||||
|
||||
let outputs = config.outputs::<OutputConfig>()?;
|
||||
renderers.extend(outputs.into_iter().map(|(key, table)| {
|
||||
let renderer = if key == "html" {
|
||||
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
||||
} else if key == "markdown" {
|
||||
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
|
||||
} else {
|
||||
let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
|
||||
Box::new(CmdRenderer::new(key.clone(), command))
|
||||
};
|
||||
(key, renderer)
|
||||
}));
|
||||
|
||||
// if we couldn't find anything, add the HTML renderer as a default
|
||||
if renderers.is_empty() {
|
||||
renderers.insert("html".to_string(), Box::new(HtmlHandlebars::new()));
|
||||
}
|
||||
|
||||
Ok(renderers)
|
||||
}
|
||||
|
||||
const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
|
||||
|
||||
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||
let name = pre.name();
|
||||
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
|
||||
}
|
||||
|
||||
/// A `preprocessor` table.
|
||||
#[derive(Deserialize)]
|
||||
struct PreprocessorConfig {
|
||||
command: Option<String>,
|
||||
#[serde(default)]
|
||||
before: Vec<String>,
|
||||
#[serde(default)]
|
||||
after: Vec<String>,
|
||||
#[serde(default)]
|
||||
optional: bool,
|
||||
}
|
||||
|
||||
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
||||
fn determine_preprocessors(
|
||||
config: &Config,
|
||||
root: &Path,
|
||||
) -> Result<IndexMap<String, Box<dyn Preprocessor>>> {
|
||||
// Collect the names of all preprocessors intended to be run, and the order
|
||||
// in which they should be run.
|
||||
let mut preprocessor_names = TopologicalSort::<String>::new();
|
||||
|
||||
if config.build.use_default_preprocessors {
|
||||
for name in DEFAULT_PREPROCESSORS {
|
||||
preprocessor_names.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let preprocessor_table = config.preprocessors::<PreprocessorConfig>()?;
|
||||
|
||||
for (name, table) in preprocessor_table.iter() {
|
||||
preprocessor_names.insert(name.to_string());
|
||||
|
||||
let exists = |name| {
|
||||
(config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
|
||||
|| preprocessor_table.contains_key(name)
|
||||
};
|
||||
|
||||
for after in &table.before {
|
||||
if !exists(&after) {
|
||||
// Only warn so that preprocessors can be toggled on and off (e.g. for
|
||||
// troubleshooting) without having to worry about order too much.
|
||||
warn!(
|
||||
"preprocessor.{}.after contains \"{}\", which was not found",
|
||||
name, after
|
||||
);
|
||||
} else {
|
||||
preprocessor_names.add_dependency(name, after);
|
||||
}
|
||||
}
|
||||
|
||||
for before in &table.after {
|
||||
if !exists(&before) {
|
||||
// See equivalent warning above for rationale
|
||||
warn!(
|
||||
"preprocessor.{}.before contains \"{}\", which was not found",
|
||||
name, before
|
||||
);
|
||||
} else {
|
||||
preprocessor_names.add_dependency(before, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that all links have been established, queue preprocessors in a suitable order
|
||||
let mut preprocessors = IndexMap::with_capacity(preprocessor_names.len());
|
||||
// `pop_all()` returns an empty vector when no more items are not being depended upon
|
||||
for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
|
||||
.take_while(|names| !names.is_empty())
|
||||
{
|
||||
// The `topological_sort` crate does not guarantee a stable order for ties, even across
|
||||
// runs of the same program. Thus, we break ties manually by sorting.
|
||||
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
|
||||
// values ([1]), which may not be an alphabetical sort.
|
||||
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
|
||||
// preprocessor execution order.
|
||||
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
|
||||
names.sort();
|
||||
for name in names {
|
||||
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
|
||||
"links" => Box::new(LinkPreprocessor::new()),
|
||||
"index" => Box::new(IndexPreprocessor::new()),
|
||||
_ => {
|
||||
// The only way to request a custom preprocessor is through the `preprocessor`
|
||||
// table, so it must exist, be a table, and contain the key.
|
||||
let table = &preprocessor_table[&name];
|
||||
let command = table
|
||||
.command
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| format!("mdbook-{name}"));
|
||||
Box::new(CmdPreprocessor::new(
|
||||
name.clone(),
|
||||
command,
|
||||
root.to_owned(),
|
||||
table.optional,
|
||||
))
|
||||
}
|
||||
};
|
||||
preprocessors.insert(name, preprocessor);
|
||||
}
|
||||
}
|
||||
|
||||
// "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
|
||||
// Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
|
||||
if preprocessor_names.is_empty() {
|
||||
Ok(preprocessors)
|
||||
} else {
|
||||
Err(Error::msg("Cyclic dependency detected in preprocessors"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether we should run a particular `Preprocessor` in combination
|
||||
/// with the renderer, falling back to `Preprocessor::supports_renderer()`
|
||||
/// method if the user doesn't say anything.
|
||||
///
|
||||
/// The `build.use-default-preprocessors` config option can be used to ensure
|
||||
/// default preprocessors always run if they support the renderer.
|
||||
fn preprocessor_should_run(
|
||||
preprocessor: &dyn Preprocessor,
|
||||
renderer: &dyn Renderer,
|
||||
cfg: &Config,
|
||||
) -> Result<bool> {
|
||||
// default preprocessors should be run by default (if supported)
|
||||
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
|
||||
return preprocessor.supports_renderer(renderer.name());
|
||||
}
|
||||
|
||||
let key = format!("preprocessor.{}.renderers", preprocessor.name());
|
||||
let renderer_name = renderer.name();
|
||||
|
||||
match cfg.get::<Vec<String>>(&key) {
|
||||
Ok(Some(explicit_renderers)) => {
|
||||
Ok(explicit_renderers.iter().any(|name| name == renderer_name))
|
||||
}
|
||||
Ok(None) => preprocessor.supports_renderer(renderer_name),
|
||||
Err(e) => bail!("failed to get `{key}`: {e}"),
|
||||
}
|
||||
}
|
||||
284
crates/mdbook-driver/src/mdbook/tests.rs
Normal file
284
crates/mdbook-driver/src/mdbook/tests.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use toml::value::{Table, Value};
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_html_renderer_if_empty() {
|
||||
let cfg = Config::default();
|
||||
|
||||
// make sure we haven't got anything in the `output` table
|
||||
assert!(cfg.outputs::<toml::Value>().unwrap().is_empty());
|
||||
|
||||
let got = determine_renderers(&cfg).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].name(), "html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_a_random_renderer_to_the_config() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.set("output.random", Table::new()).unwrap();
|
||||
|
||||
let got = determine_renderers(&cfg).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].name(), "random");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_a_random_renderer_with_custom_command_to_the_config() {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
let mut table = Table::new();
|
||||
table.insert("command".to_string(), Value::String("false".to_string()));
|
||||
cfg.set("output.random", table).unwrap();
|
||||
|
||||
let got = determine_renderers(&cfg).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].name(), "random");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
|
||||
let cfg = Config::default();
|
||||
|
||||
// make sure we haven't got anything in the `preprocessor` table
|
||||
assert!(cfg.preprocessors::<toml::Value>().unwrap().is_empty());
|
||||
|
||||
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
let names: Vec<_> = got.values().map(|p| p.name()).collect();
|
||||
assert_eq!(names, ["index", "links"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_default_preprocessors_works() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.build.use_default_preprocessors = false;
|
||||
|
||||
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_determine_third_party_preprocessors() {
|
||||
let cfg_str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
|
||||
[preprocessor.random]
|
||||
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
create-missing = false
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// make sure the `preprocessor.random` table exists
|
||||
assert!(cfg.get::<Value>("preprocessor.random").unwrap().is_some());
|
||||
|
||||
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
assert!(got.contains_key("random"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessors_can_provide_their_own_commands() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
command = "python random.py"
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// make sure the `preprocessor.random` table exists
|
||||
let random = cfg
|
||||
.get::<OutputConfig>("preprocessor.random")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(random.command, Some("python random.py".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_before_must_be_array() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = 0
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_after_must_be_array() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
after = 0
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_order_is_honored() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = [ "last" ]
|
||||
after = [ "index" ]
|
||||
|
||||
[preprocessor.last]
|
||||
after = [ "links", "index" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
let index = |name| preprocessors.get_index_of(name).unwrap();
|
||||
let assert_before = |before, after| {
|
||||
if index(before) >= index(after) {
|
||||
eprintln!("Preprocessor order:");
|
||||
for preprocessor in preprocessors.keys() {
|
||||
eprintln!(" {}", preprocessor);
|
||||
}
|
||||
panic!("{before} should come before {after}");
|
||||
}
|
||||
};
|
||||
|
||||
assert_before("index", "random");
|
||||
assert_before("index", "last");
|
||||
assert_before("random", "last");
|
||||
assert_before("links", "last");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cyclic_dependencies_are_detected() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
before = [ "index" ]
|
||||
|
||||
[preprocessor.index]
|
||||
before = [ "links" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependencies_dont_register_undefined_preprocessors() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
before = [ "random" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
// Does not contain "random"
|
||||
assert_eq!(preprocessors.keys().collect::<Vec<_>>(), ["index", "links"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = [ "links" ]
|
||||
|
||||
[build]
|
||||
use-default-preprocessors = false
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
// Does not contain "links"
|
||||
assert_eq!(preprocessors.keys().collect::<Vec<_>>(), ["random"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_respects_preprocessor_selection() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
renderers = ["html"]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let html_renderer = HtmlHandlebars::default();
|
||||
let pre = LinkPreprocessor::new();
|
||||
|
||||
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg).unwrap();
|
||||
assert!(should_run);
|
||||
}
|
||||
|
||||
struct BoolPreprocessor(bool);
|
||||
impl Preprocessor for BoolPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
"bool-preprocessor"
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, _renderer: &str) -> Result<bool> {
|
||||
Ok(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
|
||||
let cfg = Config::default();
|
||||
let html = HtmlHandlebars::new();
|
||||
|
||||
let should_be = true;
|
||||
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let should_be = false;
|
||||
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
// Default is to sort preprocessors alphabetically.
|
||||
#[test]
|
||||
fn preprocessor_sorted_by_name() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.xyz]
|
||||
[preprocessor.abc]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
let names: Vec<_> = got.values().map(|p| p.name()).collect();
|
||||
assert_eq!(names, ["abc", "index", "links", "xyz"]);
|
||||
}
|
||||
|
||||
// Default is to sort renderers alphabetically.
|
||||
#[test]
|
||||
fn renderers_sorted_by_name() {
|
||||
let cfg_str = r#"
|
||||
[output.xyz]
|
||||
[output.abc]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let got = determine_renderers(&cfg).unwrap();
|
||||
|
||||
let names: Vec<_> = got.values().map(|p| p.name()).collect();
|
||||
assert_eq!(names, ["abc", "xyz"]);
|
||||
}
|
||||
37
crates/mdbook-html/Cargo.toml
Normal file
37
crates/mdbook-html/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "mdbook-html"
|
||||
version = "0.5.2"
|
||||
description = "mdBook HTML renderer"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
ego-tree.workspace = true
|
||||
elasticlunr-rs = { workspace = true, optional = true }
|
||||
font-awesome-as-a-crate.workspace = true
|
||||
handlebars.workspace = true
|
||||
hex.workspace = true
|
||||
html5ever.workspace = true
|
||||
indexmap.workspace = true
|
||||
mdbook-core.workspace = true
|
||||
mdbook-markdown.workspace = true
|
||||
mdbook-renderer.workspace = true
|
||||
pulldown-cmark.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
search = ["dep:elasticlunr-rs"]
|
||||
13
crates/mdbook-html/README.md
Normal file
13
crates/mdbook-html/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# mdbook-html
|
||||
|
||||
[](https://docs.rs/mdbook-html)
|
||||
[](https://crates.io/crates/mdbook-html)
|
||||
[](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
|
||||
|
||||
This is the HTML renderer for [mdBook](https://rust-lang.github.io/mdBook/). This is intended for internal use only. It is automatically included by [`mdbook-driver`](https://crates.io/crates/mdbook-driver) to render books to HTML.
|
||||
|
||||
> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning.
|
||||
|
||||
## License
|
||||
|
||||
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)
|
||||
@@ -13,7 +13,6 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #5c6773;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
@@ -1,9 +1,9 @@
|
||||
/* CSS for UI elements (a.k.a. chrome) */
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--scrollbar) var(--bg);
|
||||
scrollbar-color: var(--scrollbar) transparent;
|
||||
}
|
||||
#searchresults a,
|
||||
#mdbook-searchresults a,
|
||||
.content a:link,
|
||||
a:visited,
|
||||
a > .hljs {
|
||||
@@ -11,10 +11,10 @@ a > .hljs {
|
||||
}
|
||||
|
||||
/*
|
||||
body-container is necessary because mobile browsers don't seem to like
|
||||
mdbook-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 {
|
||||
#mdbook-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
|
||||
@@ -25,12 +25,12 @@ a > .hljs {
|
||||
|
||||
/* Menu Bar */
|
||||
|
||||
#menu-bar,
|
||||
#menu-bar-hover-placeholder {
|
||||
#mdbook-menu-bar,
|
||||
#mdbook-menu-bar-hover-placeholder {
|
||||
z-index: 101;
|
||||
margin: auto calc(0px - var(--page-padding));
|
||||
}
|
||||
#menu-bar {
|
||||
#mdbook-menu-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -39,24 +39,24 @@ a > .hljs {
|
||||
border-block-end-width: 1px;
|
||||
border-block-end-style: solid;
|
||||
}
|
||||
#menu-bar.sticky,
|
||||
#menu-bar-hover-placeholder:hover + #menu-bar,
|
||||
#menu-bar:hover,
|
||||
html.sidebar-visible #menu-bar {
|
||||
#mdbook-menu-bar.sticky,
|
||||
#mdbook-menu-bar-hover-placeholder:hover + #mdbook-menu-bar,
|
||||
#mdbook-menu-bar:hover,
|
||||
html.sidebar-visible #mdbook-menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0 !important;
|
||||
}
|
||||
#menu-bar-hover-placeholder {
|
||||
#mdbook-menu-bar-hover-placeholder {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
height: var(--menu-bar-height);
|
||||
}
|
||||
#menu-bar.bordered {
|
||||
#mdbook-menu-bar.bordered {
|
||||
border-block-end-color: var(--table-border-color);
|
||||
}
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
#mdbook-menu-bar .fa-svg, #mdbook-menu-bar .icon-button {
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
z-index: 10;
|
||||
@@ -65,7 +65,7 @@ html.sidebar-visible #menu-bar {
|
||||
transition: color 0.5s;
|
||||
}
|
||||
@media only screen and (max-width: 420px) {
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
#mdbook-menu-bar .fa-svg, #mdbook-menu-bar .icon-button {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ html.sidebar-visible #menu-bar {
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.icon-button i {
|
||||
.icon-button .fa-svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -118,14 +118,14 @@ html:not(.js) .left-buttons button {
|
||||
.mobile-nav-chapters,
|
||||
.mobile-nav-chapters:visited,
|
||||
.menu-bar .icon-button,
|
||||
.menu-bar a i {
|
||||
.menu-bar a .fa-svg {
|
||||
color: var(--icons);
|
||||
}
|
||||
|
||||
.menu-bar i:hover,
|
||||
.menu-bar .fa-svg:hover,
|
||||
.menu-bar .icon-button:hover,
|
||||
.nav-chapters:hover,
|
||||
.mobile-nav-chapters i:hover {
|
||||
.mobile-nav-chapters .fa-svg:hover {
|
||||
color: var(--icons-hover);
|
||||
}
|
||||
|
||||
@@ -186,10 +186,6 @@ html:not(.js) .left-buttons button {
|
||||
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; }
|
||||
@@ -197,8 +193,8 @@ html:not(.js) .left-buttons button {
|
||||
|
||||
/* sidebar-visible */
|
||||
@media only screen and (max-width: 1380px) {
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; }
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; }
|
||||
#mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; }
|
||||
#mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
@@ -244,13 +240,10 @@ pre > .buttons :hover {
|
||||
border-color: var(--icons-hover);
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
pre > .buttons i {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
pre > .buttons button {
|
||||
cursor: inherit;
|
||||
margin: 0px 5px;
|
||||
padding: 4px 4px 3px 5px;
|
||||
padding: 2px 3px 0px 4px;
|
||||
font-size: 23px;
|
||||
|
||||
border-style: solid;
|
||||
@@ -314,7 +307,7 @@ pre > .result {
|
||||
|
||||
/* Search */
|
||||
|
||||
#searchresults a {
|
||||
#mdbook-searchresults a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -344,13 +337,13 @@ mark.fade-out {
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
|
||||
#searchbar-outer.searching #searchbar {
|
||||
#mdbook-searchbar-outer.searching #mdbook-searchbar {
|
||||
padding-right: 30px;
|
||||
}
|
||||
#searchbar-outer .spinner-wrapper {
|
||||
#mdbook-searchbar-outer .spinner-wrapper {
|
||||
display: none;
|
||||
}
|
||||
#searchbar-outer.searching .spinner-wrapper {
|
||||
#mdbook-searchbar-outer.searching .spinner-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -369,7 +362,21 @@ mark.fade-out {
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
#searchbar {
|
||||
#fa-spin {
|
||||
animation: rotating 2s linear infinite;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#mdbook-searchbar {
|
||||
width: 100%;
|
||||
margin-block-start: var(--searchbar-margin-block-start);
|
||||
margin-block-end: 0;
|
||||
@@ -382,8 +389,8 @@ mark.fade-out {
|
||||
background-color: var(--searchbar-bg);
|
||||
color: var(--searchbar-fg);
|
||||
}
|
||||
#searchbar:focus,
|
||||
#searchbar.active {
|
||||
#mdbook-searchbar:focus,
|
||||
#mdbook-searchbar.active {
|
||||
box-shadow: 0 0 3px var(--searchbar-shadow-color);
|
||||
}
|
||||
|
||||
@@ -404,19 +411,19 @@ mark.fade-out {
|
||||
border-block-end: 1px dashed var(--searchresults-border-color);
|
||||
}
|
||||
|
||||
ul#searchresults {
|
||||
ul#mdbook-searchresults {
|
||||
list-style: none;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
ul#searchresults li {
|
||||
ul#mdbook-searchresults li {
|
||||
margin: 10px 0px;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
ul#searchresults li.focus {
|
||||
ul#mdbook-searchresults li.focus {
|
||||
background-color: var(--searchresults-li-bg);
|
||||
}
|
||||
ul#searchresults span.teaser {
|
||||
ul#mdbook-searchresults span.teaser {
|
||||
display: block;
|
||||
clear: both;
|
||||
margin-block-start: 5px;
|
||||
@@ -425,7 +432,7 @@ ul#searchresults span.teaser {
|
||||
margin-inline-end: 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
ul#searchresults span.teaser em {
|
||||
ul#mdbook-searchresults span.teaser em {
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -502,9 +509,9 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
height: 16px;
|
||||
color: var(--icons);
|
||||
margin-inline-start: var(--sidebar-resize-indicator-space);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.sidebar-resize-handle .sidebar-resize-indicator::before {
|
||||
content: "";
|
||||
@@ -527,11 +534,16 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
cursor: col-resize;
|
||||
width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space));
|
||||
}
|
||||
|
||||
html:not(.js) .sidebar-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* sidebar-hidden */
|
||||
#sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
#mdbook-sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
[dir=rtl] #mdbook-sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
.sidebar::-webkit-scrollbar {
|
||||
@@ -542,18 +554,18 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
}
|
||||
|
||||
/* sidebar-visible */
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
#mdbook-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 {
|
||||
[dir=rtl] #mdbook-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-toggle-anchor:checked ~ .page-wrapper {
|
||||
#mdbook-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 {
|
||||
[dir=rtl] #mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -564,17 +576,18 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
line-height: 2.2em;
|
||||
}
|
||||
|
||||
.chapter ol {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chapter li {
|
||||
display: flex;
|
||||
color: var(--sidebar-non-existant);
|
||||
}
|
||||
|
||||
/* This is a span wrapping the chapter link and the fold chevron. */
|
||||
.chapter-link-wrapper {
|
||||
/* Used to position the chevron to the right, allowing the text to wrap before it. */
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chapter li a {
|
||||
display: block;
|
||||
padding: 0;
|
||||
/* Remove underlines. */
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
@@ -587,21 +600,22 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.chapter li > a.toggle {
|
||||
/* This is the toggle chevron. */
|
||||
.chapter-fold-toggle {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
/* Positions the chevron to the side. */
|
||||
margin-inline-start: auto;
|
||||
padding: 0 10px;
|
||||
user-select: none;
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.chapter li > a.toggle div {
|
||||
.chapter-fold-toggle div {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
/* collapse the section */
|
||||
.chapter li:not(.expanded) + li > ol {
|
||||
.chapter li:not(.expanded) > ol {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -610,10 +624,26 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
margin-block-start: 0.6em;
|
||||
}
|
||||
|
||||
.chapter li.expanded > a.toggle div {
|
||||
/* When expanded, rotate the chevron to point down. */
|
||||
.chapter li.expanded > span > .chapter-fold-toggle div {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.chapter a.current-header {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.on-this-page {
|
||||
margin-left: 22px;
|
||||
border-inline-start: 4px solid var(--sidebar-header-border-color);
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.on-this-page > ol {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* Horizontal line in chapter list. */
|
||||
.spacer {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
@@ -623,6 +653,7 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
background-color: var(--sidebar-spacer);
|
||||
}
|
||||
|
||||
/* On touch devices, add more vertical spacing to make it easier to tap links. */
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
.chapter li a { padding: 5px 0; }
|
||||
.spacer { margin: 10px 0; }
|
||||
@@ -61,7 +61,8 @@ h2:target::before,
|
||||
h3:target::before,
|
||||
h4:target::before,
|
||||
h5:target::before,
|
||||
h6:target::before {
|
||||
h6:target::before,
|
||||
dt:target::before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-inline-start: -30px;
|
||||
@@ -80,7 +81,7 @@ h6:target::before {
|
||||
.page {
|
||||
outline: 0;
|
||||
padding: 0 var(--page-padding);
|
||||
margin-block-start: 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 #mdbook-menu-bar-hover-placeholder */
|
||||
}
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
@@ -155,6 +156,8 @@ blockquote {
|
||||
border-block-end: .1em solid var(--quote-border);
|
||||
}
|
||||
|
||||
/* TODO: Remove .warning in a future version of mdbook, it is replaced by
|
||||
blockquote tags. */
|
||||
.warning {
|
||||
margin: 20px;
|
||||
padding: 0 20px;
|
||||
@@ -278,3 +281,128 @@ sup {
|
||||
.result-no-output {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.fa-svg svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
margin-bottom: -0.1em;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
/* This uses a CSS counter to add numbers to definitions, but only if there is
|
||||
more than one definition. */
|
||||
dl, dt {
|
||||
counter-reset: dd-counter;
|
||||
}
|
||||
|
||||
/* When there is more than one definition, increment the counter. The first
|
||||
selector selects the first definition, and the second one selects definitions
|
||||
2 and beyond.*/
|
||||
dd:has(+ dd), dd + dd {
|
||||
counter-increment: dd-counter;
|
||||
/* Use flex display to help with positioning the numbers when there is a p
|
||||
tag inside the definition. */
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* Shows the counter for definitions. The first selector selects the first
|
||||
definition, and the second one selections definitions 2 and beyond.*/
|
||||
dd:has(+ dd)::before, dd + dd::before {
|
||||
content: counter(dd-counter) ". ";
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
dd > p {
|
||||
/* For loose definitions that have a p tag inside, don't add a bunch of
|
||||
space before the definition. */
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Remove some excess space from the bottom. */
|
||||
.blockquote-tag p:last-child {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.blockquote-tag {
|
||||
/* Add some padding to make the vertical bar a little taller than the text.*/
|
||||
padding: 2px 0px 2px 20px;
|
||||
/* Add a solid color bar on the left side. */
|
||||
border-inline-start-style: solid;
|
||||
border-inline-start-width: 4px;
|
||||
/* Disable the background color from normal blockquotes . */
|
||||
background-color: inherit;
|
||||
/* Disable border blocks from blockquotes. */
|
||||
border-block-start: none;
|
||||
border-block-end: none;
|
||||
}
|
||||
|
||||
.blockquote-tag-title svg {
|
||||
fill: currentColor;
|
||||
/* Add space between the icon and the title. */
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.blockquote-tag-note {
|
||||
border-inline-start-color: var(--blockquote-note-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-tip {
|
||||
border-inline-start-color: var(--blockquote-tip-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-important {
|
||||
border-inline-start-color: var(--blockquote-important-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-warning {
|
||||
border-inline-start-color: var(--blockquote-warning-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-caution {
|
||||
border-inline-start-color: var(--blockquote-caution-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-note .blockquote-tag-title {
|
||||
color: var(--blockquote-note-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-tip .blockquote-tag-title {
|
||||
color: var(--blockquote-tip-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-important .blockquote-tag-title {
|
||||
color: var(--blockquote-important-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-warning .blockquote-tag-title {
|
||||
color: var(--blockquote-warning-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-caution .blockquote-tag-title {
|
||||
color: var(--blockquote-caution-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-title {
|
||||
/* Slightly increase the weight for more emphasis. */
|
||||
font-weight: 600;
|
||||
/* Vertically center the icon with the text. */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* Remove default large margins for a more compact display. */
|
||||
margin: 2px 0 8px 0;
|
||||
}
|
||||
|
||||
.blockquote-tag-title .fa-svg {
|
||||
fill: currentColor;
|
||||
/* Add some space between the icon and the text. */
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
#mdbook-sidebar,
|
||||
#mdbook-menu-bar,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#page-wrapper.page-wrapper {
|
||||
#mdbook-page-wrapper.page-wrapper {
|
||||
transform: none !important;
|
||||
margin-inline-start: 0px;
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
#content {
|
||||
#mdbook-content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -67,6 +67,14 @@
|
||||
--footnote-highlight: #2668a6;
|
||||
|
||||
--overlay-bg: rgba(33, 40, 48, 0.4);
|
||||
|
||||
--blockquote-note-color: #74b9ff;
|
||||
--blockquote-tip-color: #09ca09;
|
||||
--blockquote-important-color: #d3abff;
|
||||
--blockquote-warning-color: #f0b72f;
|
||||
--blockquote-caution-color: #f21424;
|
||||
|
||||
--sidebar-header-border-color: #c18639;
|
||||
}
|
||||
|
||||
.coal {
|
||||
@@ -120,6 +128,14 @@
|
||||
--footnote-highlight: #4079ae;
|
||||
|
||||
--overlay-bg: rgba(33, 40, 48, 0.4);
|
||||
|
||||
--blockquote-note-color: #4493f8;
|
||||
--blockquote-tip-color: #08ae08;
|
||||
--blockquote-important-color: #ab7df8;
|
||||
--blockquote-warning-color: #d29922;
|
||||
--blockquote-caution-color: #d91b29;
|
||||
|
||||
--sidebar-header-border-color: #3473ad;
|
||||
}
|
||||
|
||||
.light, html:not(.js) {
|
||||
@@ -173,6 +189,14 @@
|
||||
--footnote-highlight: #7e7eff;
|
||||
|
||||
--overlay-bg: rgba(200, 200, 205, 0.4);
|
||||
|
||||
--blockquote-note-color: #0969da;
|
||||
--blockquote-tip-color: #008000;
|
||||
--blockquote-important-color: #8250df;
|
||||
--blockquote-warning-color: #9a6700;
|
||||
--blockquote-caution-color: #b52731;
|
||||
|
||||
--sidebar-header-border-color: #6e6edb;
|
||||
}
|
||||
|
||||
.navy {
|
||||
@@ -226,6 +250,14 @@
|
||||
--footnote-highlight: #4079ae;
|
||||
|
||||
--overlay-bg: rgba(33, 40, 48, 0.4);
|
||||
|
||||
--blockquote-note-color: #4493f8;
|
||||
--blockquote-tip-color: #09ca09;
|
||||
--blockquote-important-color: #ab7df8;
|
||||
--blockquote-warning-color: #d29922;
|
||||
--blockquote-caution-color: #f21424;
|
||||
|
||||
--sidebar-header-border-color: #2f6ab5;
|
||||
}
|
||||
|
||||
.rust {
|
||||
@@ -277,6 +309,14 @@
|
||||
--footnote-highlight: #d3a17a;
|
||||
|
||||
--overlay-bg: rgba(150, 150, 150, 0.25);
|
||||
|
||||
--blockquote-note-color: #023b95;
|
||||
--blockquote-tip-color: #007700;
|
||||
--blockquote-important-color: #8250df;
|
||||
--blockquote-warning-color: #603700;
|
||||
--blockquote-caution-color: #aa1721;
|
||||
|
||||
--sidebar-header-border-color: #8c391f;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -327,5 +367,17 @@
|
||||
--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%);
|
||||
|
||||
--footnote-highlight: #4079ae;
|
||||
|
||||
--overlay-bg: rgba(33, 40, 48, 0.4);
|
||||
|
||||
--blockquote-note-color: #4493f8;
|
||||
--blockquote-tip-color: #08ae08;
|
||||
--blockquote-important-color: #ab7df8;
|
||||
--blockquote-warning-color: #d29922;
|
||||
--blockquote-caution-color: #d91b29;
|
||||
|
||||
--sidebar-header-border-color: #3473ad;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -85,7 +85,6 @@ function playground_text(playground, hidden = true) {
|
||||
const re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
||||
const snippet_crates = [];
|
||||
let item;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (item = re.exec(txt)) {
|
||||
snippet_crates.push(item[1]);
|
||||
}
|
||||
@@ -97,6 +96,7 @@ function playground_text(playground, hidden = true) {
|
||||
|
||||
if (all_available) {
|
||||
play_button.classList.remove('hidden');
|
||||
play_button.hidden = false;
|
||||
} else {
|
||||
play_button.classList.add('hidden');
|
||||
}
|
||||
@@ -207,26 +207,25 @@ function playground_text(playground, hidden = true) {
|
||||
|
||||
const buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
buttons.innerHTML = '<button class="fa fa-eye" title="Show hidden lines" \
|
||||
buttons.innerHTML = '<button title="Show hidden lines" \
|
||||
aria-label="Show hidden lines"></button>';
|
||||
buttons.firstChild.innerHTML = document.getElementById('fa-eye').innerHTML;
|
||||
|
||||
// add expand button
|
||||
const pre_block = block.parentNode;
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
|
||||
pre_block.querySelector('.buttons').addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('fa-eye')) {
|
||||
e.target.classList.remove('fa-eye');
|
||||
e.target.classList.add('fa-eye-slash');
|
||||
e.target.title = 'Hide lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
buttons.firstChild.addEventListener('click', function(e) {
|
||||
if (this.title === 'Show hidden lines') {
|
||||
this.innerHTML = document.getElementById('fa-eye-slash').innerHTML;
|
||||
this.title = 'Hide lines';
|
||||
this.setAttribute('aria-label', e.target.title);
|
||||
|
||||
block.classList.remove('hide-boring');
|
||||
} else if (e.target.classList.contains('fa-eye-slash')) {
|
||||
e.target.classList.remove('fa-eye-slash');
|
||||
e.target.classList.add('fa-eye');
|
||||
e.target.title = 'Show hidden lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
} else if (this.title === 'Hide lines') {
|
||||
this.innerHTML = document.getElementById('fa-eye').innerHTML;
|
||||
this.title = 'Show hidden lines';
|
||||
this.setAttribute('aria-label', e.target.title);
|
||||
|
||||
block.classList.add('hide-boring');
|
||||
}
|
||||
@@ -266,10 +265,11 @@ aria-label="Show hidden lines"></button>';
|
||||
}
|
||||
|
||||
const runCodeButton = document.createElement('button');
|
||||
runCodeButton.className = 'fa fa-play play-button';
|
||||
runCodeButton.className = 'play-button';
|
||||
runCodeButton.hidden = true;
|
||||
runCodeButton.title = 'Run this code';
|
||||
runCodeButton.setAttribute('aria-label', runCodeButton.title);
|
||||
runCodeButton.innerHTML = document.getElementById('fa-play').innerHTML;
|
||||
|
||||
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
||||
runCodeButton.addEventListener('click', () => {
|
||||
@@ -289,9 +289,11 @@ aria-label="Show hidden lines"></button>';
|
||||
const code_block = pre_block.querySelector('code');
|
||||
if (window.ace && code_block.classList.contains('editable')) {
|
||||
const undoChangesButton = document.createElement('button');
|
||||
undoChangesButton.className = 'fa fa-history reset-button';
|
||||
undoChangesButton.className = 'reset-button';
|
||||
undoChangesButton.title = 'Undo changes';
|
||||
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
|
||||
undoChangesButton.innerHTML +=
|
||||
document.getElementById('fa-clock-rotate-left').innerHTML;
|
||||
|
||||
buttons.insertBefore(undoChangesButton, buttons.firstChild);
|
||||
|
||||
@@ -306,23 +308,23 @@ aria-label="Show hidden lines"></button>';
|
||||
|
||||
(function themes() {
|
||||
const html = document.querySelector('html');
|
||||
const themeToggleButton = document.getElementById('theme-toggle');
|
||||
const themePopup = document.getElementById('theme-list');
|
||||
const themeToggleButton = document.getElementById('mdbook-theme-toggle');
|
||||
const themePopup = document.getElementById('mdbook-theme-list');
|
||||
const themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
|
||||
const themeIds = [];
|
||||
themePopup.querySelectorAll('button.theme').forEach(function(el) {
|
||||
themeIds.push(el.id);
|
||||
});
|
||||
const stylesheets = {
|
||||
ayuHighlight: document.querySelector('#ayu-highlight-css'),
|
||||
tomorrowNight: document.querySelector('#tomorrow-night-css'),
|
||||
highlight: document.querySelector('#highlight-css'),
|
||||
ayuHighlight: document.querySelector('#mdbook-ayu-highlight-css'),
|
||||
tomorrowNight: document.querySelector('#mdbook-tomorrow-night-css'),
|
||||
highlight: document.querySelector('#mdbook-highlight-css'),
|
||||
};
|
||||
|
||||
function showThemes() {
|
||||
themePopup.style.display = 'block';
|
||||
themeToggleButton.setAttribute('aria-expanded', true);
|
||||
themePopup.querySelector('button#' + get_theme()).focus();
|
||||
themePopup.querySelector('button#mdbook-theme-' + get_theme()).focus();
|
||||
}
|
||||
|
||||
function updateThemeSelected() {
|
||||
@@ -330,10 +332,10 @@ aria-label="Show hidden lines"></button>';
|
||||
el.classList.remove('theme-selected');
|
||||
});
|
||||
const selected = get_saved_theme() ?? 'default_theme';
|
||||
let element = themePopup.querySelector('button#' + selected);
|
||||
let element = themePopup.querySelector('button#mdbook-theme-' + selected);
|
||||
if (element === null) {
|
||||
// Fall back in case there is no "Default" item.
|
||||
element = themePopup.querySelector('button#' + get_theme());
|
||||
element = themePopup.querySelector('button#mdbook-theme-' + get_theme());
|
||||
}
|
||||
element.classList.add('theme-selected');
|
||||
}
|
||||
@@ -348,7 +350,7 @@ aria-label="Show hidden lines"></button>';
|
||||
let theme = null;
|
||||
try {
|
||||
theme = localStorage.getItem('mdbook-theme');
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore error.
|
||||
}
|
||||
return theme;
|
||||
@@ -360,7 +362,7 @@ aria-label="Show hidden lines"></button>';
|
||||
|
||||
function get_theme() {
|
||||
const theme = get_saved_theme();
|
||||
if (theme === null || theme === undefined || !themeIds.includes(theme)) {
|
||||
if (theme === null || theme === undefined || !themeIds.includes('mdbook-theme-' + theme)) {
|
||||
if (typeof default_dark_theme === 'undefined') {
|
||||
// A customized index.hbs might not define this, so fall back to
|
||||
// old behavior of determining the default on page load.
|
||||
@@ -409,7 +411,7 @@ aria-label="Show hidden lines"></button>';
|
||||
if (store) {
|
||||
try {
|
||||
localStorage.setItem('mdbook-theme', theme);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore error.
|
||||
}
|
||||
}
|
||||
@@ -445,6 +447,8 @@ aria-label="Show hidden lines"></button>';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
theme = theme.replace(/^mdbook-theme-/, '');
|
||||
|
||||
if (theme === 'default_theme' || theme === null) {
|
||||
delete_saved_theme();
|
||||
set_theme(get_theme(), false);
|
||||
@@ -515,11 +519,11 @@ aria-label="Show hidden lines"></button>';
|
||||
})();
|
||||
|
||||
(function sidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
const sidebarToggleButton = document.getElementById('sidebar-toggle');
|
||||
const sidebarResizeHandle = document.getElementById('sidebar-resize-handle');
|
||||
const sidebarCheckbox = document.getElementById('sidebar-toggle-anchor');
|
||||
const sidebar = document.getElementById('mdbook-sidebar');
|
||||
const sidebarLinks = document.querySelectorAll('#mdbook-sidebar a');
|
||||
const sidebarToggleButton = document.getElementById('mdbook-sidebar-toggle');
|
||||
const sidebarResizeHandle = document.getElementById('mdbook-sidebar-resize-handle');
|
||||
const sidebarCheckbox = document.getElementById('mdbook-sidebar-toggle-anchor');
|
||||
let firstContact = null;
|
||||
|
||||
|
||||
@@ -555,7 +559,7 @@ aria-label="Show hidden lines"></button>';
|
||||
sidebar.setAttribute('aria-hidden', false);
|
||||
try {
|
||||
localStorage.setItem('mdbook-sidebar', 'visible');
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore error.
|
||||
}
|
||||
}
|
||||
@@ -569,7 +573,7 @@ aria-label="Show hidden lines"></button>';
|
||||
sidebar.setAttribute('aria-hidden', true);
|
||||
try {
|
||||
localStorage.setItem('mdbook-sidebar', 'hidden');
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore error.
|
||||
}
|
||||
}
|
||||
@@ -780,7 +784,7 @@ aria-label="Show hidden lines"></button>';
|
||||
})();
|
||||
|
||||
(function controllMenu() {
|
||||
const menu = document.getElementById('menu-bar');
|
||||
const menu = document.getElementById('mdbook-menu-bar');
|
||||
|
||||
(function controllPosition() {
|
||||
let scrollTop = document.scrollingElement.scrollTop;
|
||||
@@ -21,14 +21,14 @@ window.search = window.search || {};
|
||||
};
|
||||
}
|
||||
|
||||
const search_wrap = document.getElementById('search-wrapper'),
|
||||
searchbar_outer = document.getElementById('searchbar-outer'),
|
||||
searchbar = document.getElementById('searchbar'),
|
||||
searchresults = document.getElementById('searchresults'),
|
||||
searchresults_outer = document.getElementById('searchresults-outer'),
|
||||
searchresults_header = document.getElementById('searchresults-header'),
|
||||
searchicon = document.getElementById('search-toggle'),
|
||||
content = document.getElementById('content'),
|
||||
const search_wrap = document.getElementById('mdbook-search-wrapper'),
|
||||
searchbar_outer = document.getElementById('mdbook-searchbar-outer'),
|
||||
searchbar = document.getElementById('mdbook-searchbar'),
|
||||
searchresults = document.getElementById('mdbook-searchresults'),
|
||||
searchresults_outer = document.getElementById('mdbook-searchresults-outer'),
|
||||
searchresults_header = document.getElementById('mdbook-searchresults-header'),
|
||||
searchicon = document.getElementById('mdbook-search-toggle'),
|
||||
content = document.getElementById('mdbook-content'),
|
||||
|
||||
// SVG text elements don't render if inside a <mark> tag.
|
||||
mark_exclude = ['text'],
|
||||
@@ -154,8 +154,9 @@ window.search = window.search || {};
|
||||
const encoded_search = encodeURIComponent(searchterms.join(' ')).replace(/'/g, '%27');
|
||||
|
||||
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + encoded_search
|
||||
+ '#' + url[1] + '" aria-details="teaser_' + teaser_count + '">'
|
||||
+ result.doc.breadcrumbs + '</a>' + '<span class="teaser" id="teaser_' + teaser_count
|
||||
+ '#' + url[1] + '" aria-details="mdbook-teaser_' + teaser_count + '">'
|
||||
+ result.doc.breadcrumbs + '</a>'
|
||||
+ '<span class="teaser" id="mdbook-teaser_' + teaser_count
|
||||
+ '" aria-label="Search Result Teaser">' + teaser + '</span>';
|
||||
}
|
||||
|
||||
@@ -437,7 +438,7 @@ window.search = window.search || {};
|
||||
loadSearchScript(
|
||||
window.path_to_searchindex_js ||
|
||||
path_to_root + '{{ resource "searchindex.js" }}',
|
||||
'search-index');
|
||||
'mdbook-search-index');
|
||||
search_wrap.classList.remove('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
@@ -33,15 +33,12 @@
|
||||
{{/if}}
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ resource "FontAwesome/css/font-awesome.css" }}">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
|
||||
{{/if}}
|
||||
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" id="highlight-css" href="{{ resource "highlight.css" }}">
|
||||
<link rel="stylesheet" id="tomorrow-night-css" href="{{ resource "tomorrow-night.css" }}">
|
||||
<link rel="stylesheet" id="ayu-highlight-css" href="{{ resource "ayu-highlight.css" }}">
|
||||
<link rel="stylesheet" id="mdbook-highlight-css" href="{{ resource "highlight.css" }}">
|
||||
<link rel="stylesheet" id="mdbook-tomorrow-night-css" href="{{ resource "tomorrow-night.css" }}">
|
||||
<link rel="stylesheet" id="mdbook-ayu-highlight-css" href="{{ resource "ayu-highlight.css" }}">
|
||||
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
@@ -79,7 +76,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="body-container">
|
||||
<div id="mdbook-body-container">
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script>
|
||||
try {
|
||||
@@ -108,12 +105,12 @@
|
||||
html.classList.add("js");
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
<input type="checkbox" id="mdbook-sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
let sidebar = null;
|
||||
const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||||
const sidebar_toggle = document.getElementById("mdbook-sidebar-toggle-anchor");
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||
sidebar = sidebar || 'visible';
|
||||
@@ -128,41 +125,41 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<nav id="mdbook-sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<!-- populated by js -->
|
||||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
||||
<noscript>
|
||||
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
|
||||
</noscript>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div id="mdbook-sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
<div id="mdbook-page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
{{> header}}
|
||||
<div id="menu-bar-hover-placeholder"></div>
|
||||
<div id="menu-bar" class="menu-bar sticky">
|
||||
<div id="mdbook-menu-bar-hover-placeholder"></div>
|
||||
<div id="mdbook-menu-bar" class="menu-bar sticky">
|
||||
<div class="left-buttons">
|
||||
<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>
|
||||
<label id="mdbook-sidebar-toggle" class="icon-button" for="mdbook-sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="mdbook-sidebar">
|
||||
{{fa "solid" "bars"}}
|
||||
</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 id="mdbook-theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="mdbook-theme-list">
|
||||
{{fa "solid" "paintbrush"}}
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</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 id="mdbook-theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-default_theme">Auto</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-light">Light</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-rust">Rust</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-coal">Coal</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-navy">Navy</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-ayu">Ayu</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="searchbar">
|
||||
<i class="fa fa-search"></i>
|
||||
<button id="mdbook-search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="mdbook-searchbar">
|
||||
{{fa "solid" "magnifying-glass"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -172,17 +169,17 @@
|
||||
<div class="right-buttons">
|
||||
{{#if print_enable}}
|
||||
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
|
||||
<i id="print-button" class="fa fa-print"></i>
|
||||
{{fa "solid" "print" "print-button"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{#if git_repository_url}}
|
||||
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
|
||||
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
|
||||
{{fa git_repository_icon_class git_repository_icon}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{#if git_repository_edit_url}}
|
||||
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit" rel="edit">
|
||||
<i id="git-edit-button" class="fa fa-edit"></i>
|
||||
{{fa "solid" "pencil" "git-edit-button"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
@@ -190,18 +187,18 @@
|
||||
</div>
|
||||
|
||||
{{#if search_enabled}}
|
||||
<div id="search-wrapper" class="hidden">
|
||||
<form id="searchbar-outer" class="searchbar-outer">
|
||||
<div id="mdbook-search-wrapper" class="hidden">
|
||||
<form id="mdbook-searchbar-outer" class="searchbar-outer">
|
||||
<div class="search-wrapper">
|
||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
<input type="search" id="mdbook-searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="mdbook-searchresults-outer" aria-describedby="searchresults-header">
|
||||
<div class="spinner-wrapper">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
{{fa "solid" "spinner" "fa-spin"}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||
<div id="searchresults-header" class="searchresults-header"></div>
|
||||
<ul id="searchresults">
|
||||
<div id="mdbook-searchresults-outer" class="searchresults-outer hidden">
|
||||
<div id="mdbook-searchresults-header" class="searchresults-header"></div>
|
||||
<ul id="mdbook-searchresults">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,31 +206,39 @@
|
||||
|
||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||
<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) {
|
||||
document.getElementById('mdbook-sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||
document.getElementById('mdbook-sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||
Array.from(document.querySelectorAll('#mdbook-sidebar a')).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="content" class="content">
|
||||
<div id="mdbook-content" class="content">
|
||||
<main>
|
||||
{{{ content }}}
|
||||
</main>
|
||||
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
{{#previous}}
|
||||
<a rel="prev" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
{{#if previous}}
|
||||
<a rel="prev" href="{{ path_to_root }}{{previous.link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
{{#if (eq ../text_direction "rtl")}}
|
||||
{{fa "solid" "angle-right"}}
|
||||
{{else}}
|
||||
{{fa "solid" "angle-left"}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/previous}}
|
||||
{{/if}}
|
||||
|
||||
{{#next}}
|
||||
<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>
|
||||
{{#if next}}
|
||||
<a rel="next prefetch" href="{{ path_to_root }}{{next.link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
{{#if (eq ../text_direction "rtl")}}
|
||||
{{fa "solid" "angle-left"}}
|
||||
{{else}}
|
||||
{{fa "solid" "angle-right"}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/next}}
|
||||
{{/if}}
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
@@ -241,21 +246,35 @@
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
{{#previous}}
|
||||
<a rel="prev" href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
{{#if previous}}
|
||||
<a rel="prev" href="{{ path_to_root }}{{previous.link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
{{#if (eq ../text_direction "rtl")}}
|
||||
{{fa "solid" "angle-right"}}
|
||||
{{else}}
|
||||
{{fa "solid" "angle-left"}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/previous}}
|
||||
{{/if}}
|
||||
|
||||
{{#next}}
|
||||
<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>
|
||||
{{#if next}}
|
||||
<a rel="next prefetch" href="{{ path_to_root }}{{next.link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
{{#if (eq text_direction "rtl")}}
|
||||
{{fa "solid" "angle-left"}}
|
||||
{{else}}
|
||||
{{fa "solid" "angle-right"}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/next}}
|
||||
{{/if}}
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
<template id=fa-eye>{{fa "solid" "eye"}}</template>
|
||||
<template id=fa-eye-slash>{{fa "solid" "eye-slash"}}</template>
|
||||
<template id=fa-copy>{{fa "regular" "copy"}}</template>
|
||||
<template id=fa-play>{{fa "solid" "play"}}</template>
|
||||
<template id=fa-clock-rotate-left>{{fa "solid" "clock-rotate-left"}}</template>
|
||||
|
||||
{{#if live_reload_endpoint}}
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
<script>
|
||||
@@ -275,25 +294,6 @@
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if google_analytics}}
|
||||
<!-- Google Analytics Tag -->
|
||||
<script>
|
||||
const localAddrs = ["localhost", "127.0.0.1", ""];
|
||||
|
||||
// make sure we don't activate google analytics if the developer is
|
||||
// inspecting the book locally...
|
||||
if (localAddrs.indexOf(document.location.hostname) === -1) {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '{{google_analytics}}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_line_numbers}}
|
||||
<script>
|
||||
window.playground_line_numbers = true;
|
||||
@@ -28,10 +28,7 @@
|
||||
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
|
||||
{{/if}}
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ resource "FontAwesome/css/font-awesome.css" }}">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
|
||||
{{/if}}
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ resource this }}">
|
||||
456
crates/mdbook-html/front-end/templates/toc.js.hbs
Normal file
456
crates/mdbook-html/front-end/templates/toc.js.hbs
Normal file
@@ -0,0 +1,456 @@
|
||||
// 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).
|
||||
class MDBookSidebarScrollbox extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
connectedCallback() {
|
||||
this.innerHTML = '{{#toc}}{{/toc}}';
|
||||
// Set the current, active page, and reveal it if it's hidden
|
||||
let current_page = document.location.href.toString().split('#')[0].split('?')[0];
|
||||
if (current_page.endsWith('/')) {
|
||||
current_page += 'index.html';
|
||||
}
|
||||
const links = Array.prototype.slice.call(this.querySelectorAll('a'));
|
||||
const l = links.length;
|
||||
for (let i = 0; i < l; ++i) {
|
||||
const link = links[i];
|
||||
const 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');
|
||||
let parent = link.parentElement;
|
||||
while (parent) {
|
||||
if (parent.tagName === 'LI' && parent.classList.contains('chapter-item')) {
|
||||
parent.classList.add('expanded');
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Track and set sidebar scroll position
|
||||
this.addEventListener('click', e => {
|
||||
if (e.target.tagName === 'A') {
|
||||
const clientRect = e.target.getBoundingClientRect();
|
||||
const sidebarRect = this.getBoundingClientRect();
|
||||
sessionStorage.setItem('sidebar-scroll-offset', clientRect.top - sidebarRect.top);
|
||||
}
|
||||
}, { passive: true });
|
||||
const sidebarScrollOffset = sessionStorage.getItem('sidebar-scroll-offset');
|
||||
sessionStorage.removeItem('sidebar-scroll-offset');
|
||||
if (sidebarScrollOffset !== null) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
const activeSection = this.querySelector('.active');
|
||||
if (activeSection) {
|
||||
const clientRect = activeSection.getBoundingClientRect();
|
||||
const sidebarRect = this.getBoundingClientRect();
|
||||
const currentOffset = clientRect.top - sidebarRect.top;
|
||||
this.scrollTop += currentOffset - parseFloat(sidebarScrollOffset);
|
||||
}
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via
|
||||
// 'next/previous chapter' buttons
|
||||
const activeSection = document.querySelector('#mdbook-sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
// Toggle buttons
|
||||
const sidebarAnchorToggles = document.querySelectorAll('.chapter-fold-toggle');
|
||||
function toggleSection(ev) {
|
||||
ev.currentTarget.parentElement.parentElement.classList.toggle('expanded');
|
||||
}
|
||||
Array.from(sidebarAnchorToggles).forEach(el => {
|
||||
el.addEventListener('click', toggleSection);
|
||||
});
|
||||
}
|
||||
}
|
||||
window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox);
|
||||
|
||||
{{#if sidebar_header_nav}}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Support for dynamically adding headers to the sidebar.
|
||||
|
||||
(function() {
|
||||
// This is used to detect which direction the page has scrolled since the
|
||||
// last scroll event.
|
||||
let lastKnownScrollPosition = 0;
|
||||
// This is the threshold in px from the top of the screen where it will
|
||||
// consider a header the "current" header when scrolling down.
|
||||
const defaultDownThreshold = 150;
|
||||
// Same as defaultDownThreshold, except when scrolling up.
|
||||
const defaultUpThreshold = 300;
|
||||
// The threshold is a virtual horizontal line on the screen where it
|
||||
// considers the "current" header to be above the line. The threshold is
|
||||
// modified dynamically to handle headers that are near the bottom of the
|
||||
// screen, and to slightly offset the behavior when scrolling up vs down.
|
||||
let threshold = defaultDownThreshold;
|
||||
// This is used to disable updates while scrolling. This is needed when
|
||||
// clicking the header in the sidebar, which triggers a scroll event. It
|
||||
// is somewhat finicky to detect when the scroll has finished, so this
|
||||
// uses a relatively dumb system of disabling scroll updates for a short
|
||||
// time after the click.
|
||||
let disableScroll = false;
|
||||
// Array of header elements on the page.
|
||||
let headers;
|
||||
// Array of li elements that are initially collapsed headers in the sidebar.
|
||||
// I'm not sure why eslint seems to have a false positive here.
|
||||
// eslint-disable-next-line prefer-const
|
||||
let headerToggles = [];
|
||||
// This is a debugging tool for the threshold which you can enable in the console.
|
||||
let thresholdDebug = false;
|
||||
|
||||
// Updates the threshold based on the scroll position.
|
||||
function updateThreshold() {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const windowHeight = window.innerHeight;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
|
||||
// The number of pixels below the viewport, at most documentHeight.
|
||||
// This is used to push the threshold down to the bottom of the page
|
||||
// as the user scrolls towards the bottom.
|
||||
const pixelsBelow = Math.max(0, documentHeight - (scrollTop + windowHeight));
|
||||
// The number of pixels above the viewport, at least defaultDownThreshold.
|
||||
// Similar to pixelsBelow, this is used to push the threshold back towards
|
||||
// the top when reaching the top of the page.
|
||||
const pixelsAbove = Math.max(0, defaultDownThreshold - scrollTop);
|
||||
// How much the threshold should be offset once it gets close to the
|
||||
// bottom of the page.
|
||||
const bottomAdd = Math.max(0, windowHeight - pixelsBelow - defaultDownThreshold);
|
||||
let adjustedBottomAdd = bottomAdd;
|
||||
|
||||
// Adjusts bottomAdd for a small document. The calculation above
|
||||
// assumes the document is at least twice the windowheight in size. If
|
||||
// it is less than that, then bottomAdd needs to be shrunk
|
||||
// proportional to the difference in size.
|
||||
if (documentHeight < windowHeight * 2) {
|
||||
const maxPixelsBelow = documentHeight - windowHeight;
|
||||
const t = 1 - pixelsBelow / Math.max(1, maxPixelsBelow);
|
||||
const clamp = Math.max(0, Math.min(1, t));
|
||||
adjustedBottomAdd *= clamp;
|
||||
}
|
||||
|
||||
let scrollingDown = true;
|
||||
if (scrollTop < lastKnownScrollPosition) {
|
||||
scrollingDown = false;
|
||||
}
|
||||
|
||||
if (scrollingDown) {
|
||||
// When scrolling down, move the threshold up towards the default
|
||||
// downwards threshold position. If near the bottom of the page,
|
||||
// adjustedBottomAdd will offset the threshold towards the bottom
|
||||
// of the page.
|
||||
const amountScrolledDown = scrollTop - lastKnownScrollPosition;
|
||||
const adjustedDefault = defaultDownThreshold + adjustedBottomAdd;
|
||||
threshold = Math.max(adjustedDefault, threshold - amountScrolledDown);
|
||||
} else {
|
||||
// When scrolling up, move the threshold down towards the default
|
||||
// upwards threshold position. If near the bottom of the page,
|
||||
// quickly transition the threshold back up where it normally
|
||||
// belongs.
|
||||
const amountScrolledUp = lastKnownScrollPosition - scrollTop;
|
||||
const adjustedDefault = defaultUpThreshold - pixelsAbove
|
||||
+ Math.max(0, adjustedBottomAdd - defaultDownThreshold);
|
||||
threshold = Math.min(adjustedDefault, threshold + amountScrolledUp);
|
||||
}
|
||||
|
||||
if (documentHeight <= windowHeight) {
|
||||
threshold = 0;
|
||||
}
|
||||
|
||||
if (thresholdDebug) {
|
||||
const id = 'mdbook-threshold-debug-data';
|
||||
let data = document.getElementById(id);
|
||||
if (data === null) {
|
||||
data = document.createElement('div');
|
||||
data.id = id;
|
||||
data.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
right: 10px;
|
||||
background-color: 0xeeeeee;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
`;
|
||||
document.body.appendChild(data);
|
||||
}
|
||||
data.innerHTML = `
|
||||
<table>
|
||||
<tr><td>documentHeight</td><td>${documentHeight.toFixed(1)}</td></tr>
|
||||
<tr><td>windowHeight</td><td>${windowHeight.toFixed(1)}</td></tr>
|
||||
<tr><td>scrollTop</td><td>${scrollTop.toFixed(1)}</td></tr>
|
||||
<tr><td>pixelsAbove</td><td>${pixelsAbove.toFixed(1)}</td></tr>
|
||||
<tr><td>pixelsBelow</td><td>${pixelsBelow.toFixed(1)}</td></tr>
|
||||
<tr><td>bottomAdd</td><td>${bottomAdd.toFixed(1)}</td></tr>
|
||||
<tr><td>adjustedBottomAdd</td><td>${adjustedBottomAdd.toFixed(1)}</td></tr>
|
||||
<tr><td>scrollingDown</td><td>${scrollingDown}</td></tr>
|
||||
<tr><td>threshold</td><td>${threshold.toFixed(1)}</td></tr>
|
||||
</table>
|
||||
`;
|
||||
drawDebugLine();
|
||||
}
|
||||
|
||||
lastKnownScrollPosition = scrollTop;
|
||||
}
|
||||
|
||||
function drawDebugLine() {
|
||||
if (!document.body) {
|
||||
return;
|
||||
}
|
||||
const id = 'mdbook-threshold-debug-line';
|
||||
const existingLine = document.getElementById(id);
|
||||
if (existingLine) {
|
||||
existingLine.remove();
|
||||
}
|
||||
const line = document.createElement('div');
|
||||
line.id = id;
|
||||
line.style.cssText = `
|
||||
position: fixed;
|
||||
top: ${threshold}px;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 2px;
|
||||
background-color: red;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
`;
|
||||
document.body.appendChild(line);
|
||||
}
|
||||
|
||||
function mdbookEnableThresholdDebug() {
|
||||
thresholdDebug = true;
|
||||
updateThreshold();
|
||||
drawDebugLine();
|
||||
}
|
||||
|
||||
window.mdbookEnableThresholdDebug = mdbookEnableThresholdDebug;
|
||||
|
||||
// Updates which headers in the sidebar should be expanded. If the current
|
||||
// header is inside a collapsed group, then it, and all its parents should
|
||||
// be expanded.
|
||||
function updateHeaderExpanded(currentA) {
|
||||
// Add expanded to all header-item li ancestors.
|
||||
let current = currentA.parentElement;
|
||||
while (current) {
|
||||
if (current.tagName === 'LI' && current.classList.contains('header-item')) {
|
||||
current.classList.add('expanded');
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
// Updates which header is marked as the "current" header in the sidebar.
|
||||
// This is done with a virtual Y threshold, where headers at or below
|
||||
// that line will be considered the current one.
|
||||
function updateCurrentHeader() {
|
||||
if (!headers || !headers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the classes, which will be rebuilt below.
|
||||
const els = document.getElementsByClassName('current-header');
|
||||
for (const el of els) {
|
||||
el.classList.remove('current-header');
|
||||
}
|
||||
for (const toggle of headerToggles) {
|
||||
toggle.classList.remove('expanded');
|
||||
}
|
||||
|
||||
// Find the last header that is above the threshold.
|
||||
let lastHeader = null;
|
||||
for (const header of headers) {
|
||||
const rect = header.getBoundingClientRect();
|
||||
if (rect.top <= threshold) {
|
||||
lastHeader = header;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastHeader === null) {
|
||||
lastHeader = headers[0];
|
||||
const rect = lastHeader.getBoundingClientRect();
|
||||
const windowHeight = window.innerHeight;
|
||||
if (rect.top >= windowHeight) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the anchor in the summary.
|
||||
const href = '#' + lastHeader.id;
|
||||
const a = [...document.querySelectorAll('.header-in-summary')]
|
||||
.find(element => element.getAttribute('href') === href);
|
||||
if (!a) {
|
||||
return;
|
||||
}
|
||||
|
||||
a.classList.add('current-header');
|
||||
|
||||
updateHeaderExpanded(a);
|
||||
}
|
||||
|
||||
// Updates which header is "current" based on the threshold line.
|
||||
function reloadCurrentHeader() {
|
||||
if (disableScroll) {
|
||||
return;
|
||||
}
|
||||
updateThreshold();
|
||||
updateCurrentHeader();
|
||||
}
|
||||
|
||||
|
||||
// When clicking on a header in the sidebar, this adjusts the threshold so
|
||||
// that it is located next to the header. This is so that header becomes
|
||||
// "current".
|
||||
function headerThresholdClick(event) {
|
||||
// See disableScroll description why this is done.
|
||||
disableScroll = true;
|
||||
setTimeout(() => {
|
||||
disableScroll = false;
|
||||
}, 100);
|
||||
// requestAnimationFrame is used to delay the update of the "current"
|
||||
// header until after the scroll is done, and the header is in the new
|
||||
// position.
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// Closest is needed because if it has child elements like <code>.
|
||||
const a = event.target.closest('a');
|
||||
const href = a.getAttribute('href');
|
||||
const targetId = href.substring(1);
|
||||
const targetElement = document.getElementById(targetId);
|
||||
if (targetElement) {
|
||||
threshold = targetElement.getBoundingClientRect().bottom;
|
||||
updateCurrentHeader();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Takes the nodes from the given head and copies them over to the
|
||||
// destination, along with some filtering.
|
||||
function filterHeader(source, dest) {
|
||||
const clone = source.cloneNode(true);
|
||||
clone.querySelectorAll('mark').forEach(mark => {
|
||||
mark.replaceWith(...mark.childNodes);
|
||||
});
|
||||
dest.append(...clone.childNodes);
|
||||
}
|
||||
|
||||
// Scans page for headers and adds them to the sidebar.
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const activeSection = document.querySelector('#mdbook-sidebar .active');
|
||||
if (activeSection === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const main = document.getElementsByTagName('main')[0];
|
||||
headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6'))
|
||||
.filter(h => h.id !== '' && h.children.length && h.children[0].tagName === 'A');
|
||||
|
||||
if (headers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a tree of headers in the sidebar.
|
||||
|
||||
const stack = [];
|
||||
|
||||
const firstLevel = parseInt(headers[0].tagName.charAt(1));
|
||||
for (let i = 1; i < firstLevel; i++) {
|
||||
const ol = document.createElement('ol');
|
||||
ol.classList.add('section');
|
||||
if (stack.length > 0) {
|
||||
stack[stack.length - 1].ol.appendChild(ol);
|
||||
}
|
||||
stack.push({level: i + 1, ol: ol});
|
||||
}
|
||||
|
||||
// The level where it will start folding deeply nested headers.
|
||||
const foldLevel = 3;
|
||||
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const header = headers[i];
|
||||
const level = parseInt(header.tagName.charAt(1));
|
||||
|
||||
const currentLevel = stack[stack.length - 1].level;
|
||||
if (level > currentLevel) {
|
||||
// Begin nesting to this level.
|
||||
for (let nextLevel = currentLevel + 1; nextLevel <= level; nextLevel++) {
|
||||
const ol = document.createElement('ol');
|
||||
ol.classList.add('section');
|
||||
const last = stack[stack.length - 1];
|
||||
const lastChild = last.ol.lastChild;
|
||||
// Handle the case where jumping more than one nesting
|
||||
// level, which doesn't have a list item to place this new
|
||||
// list inside of.
|
||||
if (lastChild) {
|
||||
lastChild.appendChild(ol);
|
||||
} else {
|
||||
last.ol.appendChild(ol);
|
||||
}
|
||||
stack.push({level: nextLevel, ol: ol});
|
||||
}
|
||||
} else if (level < currentLevel) {
|
||||
while (stack.length > 1 && stack[stack.length - 1].level > level) {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.classList.add('header-item');
|
||||
li.classList.add('expanded');
|
||||
if (level < foldLevel) {
|
||||
li.classList.add('expanded');
|
||||
}
|
||||
const span = document.createElement('span');
|
||||
span.classList.add('chapter-link-wrapper');
|
||||
const a = document.createElement('a');
|
||||
span.appendChild(a);
|
||||
a.href = '#' + header.id;
|
||||
a.classList.add('header-in-summary');
|
||||
filterHeader(header.children[0], a);
|
||||
a.addEventListener('click', headerThresholdClick);
|
||||
const nextHeader = headers[i + 1];
|
||||
if (nextHeader !== undefined) {
|
||||
const nextLevel = parseInt(nextHeader.tagName.charAt(1));
|
||||
if (nextLevel > level && level >= foldLevel) {
|
||||
const toggle = document.createElement('a');
|
||||
toggle.classList.add('chapter-fold-toggle');
|
||||
toggle.classList.add('header-toggle');
|
||||
toggle.addEventListener('click', () => {
|
||||
li.classList.toggle('expanded');
|
||||
});
|
||||
const toggleDiv = document.createElement('div');
|
||||
toggleDiv.textContent = '❱';
|
||||
toggle.appendChild(toggleDiv);
|
||||
span.appendChild(toggle);
|
||||
headerToggles.push(li);
|
||||
}
|
||||
}
|
||||
li.appendChild(span);
|
||||
|
||||
const currentParent = stack[stack.length - 1];
|
||||
currentParent.ol.appendChild(li);
|
||||
}
|
||||
|
||||
const onThisPage = document.createElement('div');
|
||||
onThisPage.classList.add('on-this-page');
|
||||
onThisPage.append(stack[0].ol);
|
||||
const activeItemSpan = activeSection.parentElement;
|
||||
activeItemSpan.after(onThisPage);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', reloadCurrentHeader);
|
||||
document.addEventListener('scroll', reloadCurrentHeader, { passive: true });
|
||||
})();
|
||||
|
||||
{{/if}}
|
||||
26
crates/mdbook-html/src/html/admonitions.rs
Normal file
26
crates/mdbook-html/src/html/admonitions.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use pulldown_cmark::BlockQuoteKind;
|
||||
|
||||
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
|
||||
const ICON_NOTE: &str = r#"<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>"#;
|
||||
|
||||
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
|
||||
const ICON_TIP: &str = r#"<path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path>"#;
|
||||
|
||||
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
|
||||
const ICON_IMPORTANT: &str = r#"<path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path>"#;
|
||||
|
||||
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
|
||||
const ICON_WARNING: &str = r#"<path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path>"#;
|
||||
|
||||
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
|
||||
const ICON_CAUTION: &str = r#"<path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>"#;
|
||||
|
||||
pub(crate) fn select_tag(kind: BlockQuoteKind) -> (&'static str, &'static str, &'static str) {
|
||||
match kind {
|
||||
BlockQuoteKind::Note => ("note", ICON_NOTE, "Note"),
|
||||
BlockQuoteKind::Tip => ("tip", ICON_TIP, "Tip"),
|
||||
BlockQuoteKind::Important => ("important", ICON_IMPORTANT, "Important"),
|
||||
BlockQuoteKind::Warning => ("warning", ICON_WARNING, "Warning"),
|
||||
BlockQuoteKind::Caution => ("caution", ICON_CAUTION, "Caution"),
|
||||
}
|
||||
}
|
||||
193
crates/mdbook-html/src/html/hide_lines.rs
Normal file
193
crates/mdbook-html/src/html/hide_lines.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
//! Support for hiding code lines.
|
||||
|
||||
use crate::html::{Element, Node};
|
||||
use ego_tree::{NodeId, Tree};
|
||||
use html5ever::tendril::StrTendril;
|
||||
use mdbook_core::static_regex;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Wraps hidden lines in a `<span>` for the given code block.
|
||||
pub(crate) fn hide_lines(
|
||||
tree: &mut Tree<Node>,
|
||||
code_id: NodeId,
|
||||
hidelines: &HashMap<String, String>,
|
||||
) {
|
||||
let mut node = tree.get_mut(code_id).unwrap();
|
||||
let el = node.value().as_element().unwrap();
|
||||
|
||||
let classes: Vec<_> = el.attr("class").unwrap_or_default().split(' ').collect();
|
||||
let language = classes
|
||||
.iter()
|
||||
.filter_map(|cls| cls.strip_prefix("language-"))
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let hideline_info = classes
|
||||
.iter()
|
||||
.filter_map(|cls| cls.strip_prefix("hidelines="))
|
||||
.map(|prefix| prefix.to_string())
|
||||
.next();
|
||||
|
||||
if let Some(mut child) = node.first_child()
|
||||
&& let Node::Text(text) = child.value()
|
||||
{
|
||||
if language == "rust" {
|
||||
let new_nodes = hide_lines_rust(text);
|
||||
child.detach();
|
||||
let root = tree.extend_tree(new_nodes);
|
||||
let root_id = root.id();
|
||||
let mut node = tree.get_mut(code_id).unwrap();
|
||||
node.reparent_from_id_append(root_id);
|
||||
} else {
|
||||
// Use the prefix from the code block, else the prefix from config.
|
||||
let hidelines_prefix = hideline_info
|
||||
.as_deref()
|
||||
.or_else(|| hidelines.get(&language).map(|p| p.as_str()));
|
||||
if let Some(prefix) = hidelines_prefix {
|
||||
let new_nodes = hide_lines_with_prefix(text, prefix);
|
||||
child.detach();
|
||||
let root = tree.extend_tree(new_nodes);
|
||||
let root_id = root.id();
|
||||
let mut node = tree.get_mut(code_id).unwrap();
|
||||
node.reparent_from_id_append(root_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps hidden lines in a `<span>` specifically for Rust code blocks.
|
||||
fn hide_lines_rust(text: &StrTendril) -> Tree<Node> {
|
||||
static_regex!(BORING_LINES_REGEX, r"^(\s*)#(.?)(.*)$");
|
||||
|
||||
let mut tree = Tree::new(Node::Fragment);
|
||||
let mut root = tree.root_mut();
|
||||
let mut lines = text.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] == "#" {
|
||||
root.append(Node::Text(
|
||||
format!("{}{}{}{newline}", &caps[1], &caps[2], &caps[3]).into(),
|
||||
));
|
||||
continue;
|
||||
} else if matches!(&caps[2], "" | " ") {
|
||||
let mut span = Element::new("span");
|
||||
span.insert_attr("class", "boring".into());
|
||||
let mut span = root.append(Node::Element(span));
|
||||
span.append(Node::Text(
|
||||
format!("{}{}{newline}", &caps[1], &caps[3]).into(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
root.append(Node::Text(format!("{line}{newline}").into()));
|
||||
}
|
||||
tree
|
||||
}
|
||||
|
||||
/// Wraps hidden lines in a `<span>` tag for lines starting with the given prefix.
|
||||
fn hide_lines_with_prefix(content: &str, prefix: &str) -> Tree<Node> {
|
||||
let mut tree = Tree::new(Node::Fragment);
|
||||
let mut root = tree.root_mut();
|
||||
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()..]);
|
||||
let mut span = Element::new("span");
|
||||
span.insert_attr("class", "boring".into());
|
||||
let mut span = root.append(Node::Element(span));
|
||||
span.append(Node::Text(format!("{ws}{rest}\n").into()));
|
||||
} else {
|
||||
root.append(Node::Text(format!("{line}\n").into()));
|
||||
}
|
||||
}
|
||||
tree
|
||||
}
|
||||
|
||||
/// If this code text is missing an `fn main`, the wrap it with `fn main` in a
|
||||
/// fashion similar to rustdoc, with the wrapper hidden.
|
||||
pub(crate) fn wrap_rust_main(text: &str) -> Option<String> {
|
||||
if !text.contains("fn main") && !text.contains("quick_main!") {
|
||||
let (attrs, code) = partition_rust_source(text);
|
||||
let newline = if code.is_empty() || code.ends_with('\n') {
|
||||
""
|
||||
} else {
|
||||
"\n"
|
||||
};
|
||||
Some(format!(
|
||||
"# #![allow(unused)]\n{attrs}# fn main() {{\n{code}{newline}# }}"
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits Rust inner attributes from the given source string.
|
||||
///
|
||||
/// Returns `(inner_attrs, rest_of_code)`.
|
||||
fn partition_rust_source(s: &str) -> (&str, &str) {
|
||||
static_regex!(
|
||||
HEADER_RE,
|
||||
r"^(?mx)
|
||||
(
|
||||
(?:
|
||||
^[ \t]*\#!\[.* (?:\r?\n)?
|
||||
|
|
||||
^\s* (?:\r?\n)?
|
||||
)*
|
||||
)"
|
||||
);
|
||||
let split_idx = match HEADER_RE.captures(s) {
|
||||
Some(caps) => {
|
||||
let attributes = &caps[1];
|
||||
if attributes.trim().is_empty() {
|
||||
// Don't include pure whitespace as an attribute. The
|
||||
// whitespace in the regex is intended to handle multiple
|
||||
// attributes *separated* by potential whitespace.
|
||||
0
|
||||
} else {
|
||||
attributes.len()
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
s.split_at(split_idx)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_partitions_rust_source() {
|
||||
assert_eq!(partition_rust_source(""), ("", ""));
|
||||
assert_eq!(partition_rust_source("let x = 1;"), ("", "let x = 1;"));
|
||||
assert_eq!(
|
||||
partition_rust_source("fn main()\n{ let x = 1; }\n"),
|
||||
("", "fn main()\n{ let x = 1; }\n")
|
||||
);
|
||||
assert_eq!(
|
||||
partition_rust_source("#![allow(foo)]"),
|
||||
("#![allow(foo)]", "")
|
||||
);
|
||||
assert_eq!(
|
||||
partition_rust_source("#![allow(foo)]\n"),
|
||||
("#![allow(foo)]\n", "")
|
||||
);
|
||||
assert_eq!(
|
||||
partition_rust_source("#![allow(foo)]\nlet x = 1;"),
|
||||
("#![allow(foo)]\n", "let x = 1;")
|
||||
);
|
||||
assert_eq!(
|
||||
partition_rust_source(
|
||||
"\n\
|
||||
#![allow(foo)]\n\
|
||||
\n\
|
||||
#![allow(bar)]\n\
|
||||
\n\
|
||||
let x = 1;"
|
||||
),
|
||||
("\n#![allow(foo)]\n\n#![allow(bar)]\n\n", "let x = 1;")
|
||||
);
|
||||
assert_eq!(
|
||||
partition_rust_source(" // Example"),
|
||||
("", " // Example")
|
||||
);
|
||||
}
|
||||
108
crates/mdbook-html/src/html/mod.rs
Normal file
108
crates/mdbook-html/src/html/mod.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
//! HTML rendering support.
|
||||
//!
|
||||
//! This module's primary entry point is [`render_markdown`] which will take
|
||||
//! markdown text and render it to HTML. In summary, the general procedure of
|
||||
//! that function is:
|
||||
//!
|
||||
//! 1. Use [`pulldown_cmark`] to parse the markdown and generate events.
|
||||
//! 2. [`tree`] converts those events to a tree data structure.
|
||||
//! 1. Parse HTML inside the markdown using [`tokenizer`].
|
||||
//! 2. Apply various transformations to the tree data structure, such as adding header links.
|
||||
//! 3. Serialize the tree to HTML in [`serialize()`].
|
||||
|
||||
use ego_tree::Tree;
|
||||
use mdbook_core::book::{Book, Chapter};
|
||||
use mdbook_core::config::{HtmlConfig, RustEdition};
|
||||
use mdbook_markdown::{MarkdownOptions, new_cmark_parser};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
mod admonitions;
|
||||
mod hide_lines;
|
||||
mod print;
|
||||
mod serialize;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod tokenizer;
|
||||
mod tree;
|
||||
|
||||
pub(crate) use hide_lines::{hide_lines, wrap_rust_main};
|
||||
pub(crate) use print::render_print_page;
|
||||
pub(crate) use serialize::serialize;
|
||||
pub(crate) use tree::{Element, Node};
|
||||
|
||||
/// Options for converting a single chapter's markdown to HTML.
|
||||
pub(crate) struct HtmlRenderOptions<'a> {
|
||||
/// Options for parsing markdown.
|
||||
pub markdown_options: MarkdownOptions,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
pub path: &'a Path,
|
||||
/// The default Rust edition, used to set the proper class on the code blocks.
|
||||
pub edition: Option<RustEdition>,
|
||||
/// The [`HtmlConfig`], whose options affect how the HTML is generated.
|
||||
pub config: &'a HtmlConfig,
|
||||
}
|
||||
|
||||
impl<'a> HtmlRenderOptions<'a> {
|
||||
/// Creates a new [`HtmlRenderOptions`].
|
||||
pub(crate) fn new(
|
||||
path: &'a Path,
|
||||
config: &'a HtmlConfig,
|
||||
edition: Option<RustEdition>,
|
||||
) -> HtmlRenderOptions<'a> {
|
||||
let mut markdown_options = MarkdownOptions::default();
|
||||
markdown_options.smart_punctuation = config.smart_punctuation;
|
||||
markdown_options.definition_lists = config.definition_lists;
|
||||
markdown_options.admonitions = config.admonitions;
|
||||
HtmlRenderOptions {
|
||||
markdown_options,
|
||||
path,
|
||||
edition,
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders markdown to HTML.
|
||||
pub(crate) fn render_markdown(text: &str, options: &HtmlRenderOptions<'_>) -> String {
|
||||
let tree = build_tree(text, options);
|
||||
let mut output = String::new();
|
||||
serialize::serialize(&tree, &mut output);
|
||||
output
|
||||
}
|
||||
|
||||
/// Renders markdown to a [`Tree`].
|
||||
fn build_tree(text: &str, options: &HtmlRenderOptions<'_>) -> Tree<Node> {
|
||||
let events = new_cmark_parser(text, &options.markdown_options);
|
||||
tree::MarkdownTreeBuilder::build(options, events)
|
||||
}
|
||||
|
||||
/// The parsed chapter, and some information about the chapter.
|
||||
pub(crate) struct ChapterTree<'book> {
|
||||
pub(crate) chapter: &'book Chapter,
|
||||
/// The path to the chapter relative to the root with the `.html` extension.
|
||||
pub(crate) html_path: PathBuf,
|
||||
/// The chapter tree.
|
||||
pub(crate) tree: Tree<Node>,
|
||||
}
|
||||
|
||||
/// Creates all of the [`ChapterTree`]s for the book.
|
||||
pub(crate) fn build_trees<'book>(
|
||||
book: &'book Book,
|
||||
html_config: &HtmlConfig,
|
||||
edition: Option<RustEdition>,
|
||||
) -> Vec<ChapterTree<'book>> {
|
||||
book.chapters()
|
||||
.map(|ch| {
|
||||
let path = ch.path.as_ref().unwrap();
|
||||
let html_path = ch.path.as_ref().unwrap().with_extension("html");
|
||||
let options = HtmlRenderOptions::new(path, html_config, edition);
|
||||
let tree = build_tree(&ch.content, &options);
|
||||
|
||||
ChapterTree {
|
||||
chapter: ch,
|
||||
html_path,
|
||||
tree,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
215
crates/mdbook-html/src/html/print.rs
Normal file
215
crates/mdbook-html/src/html/print.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
//! Support for generating the print page.
|
||||
//!
|
||||
//! The print page takes all the individual chapters (as `Tree<Node>`
|
||||
//! elements) and modifies the chapters so that they work on a consolidated
|
||||
//! print page, and then serializes it all as one HTML page.
|
||||
|
||||
use super::Node;
|
||||
use crate::html::{ChapterTree, Element, serialize};
|
||||
use crate::utils::{ToUrlPath, id_from_content, normalize_path, unique_id};
|
||||
use mdbook_core::static_regex;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Takes all the chapter trees, modifies them to be suitable to render for
|
||||
/// the print page, and returns an string of all the chapters rendered to a
|
||||
/// single HTML page.
|
||||
pub(crate) fn render_print_page(mut chapter_trees: Vec<ChapterTree<'_>>) -> String {
|
||||
let (id_remap, mut id_counter) = make_ids_unique(&mut chapter_trees);
|
||||
let path_to_root_id = make_root_id_map(&mut chapter_trees, &mut id_counter);
|
||||
rewrite_links(&mut chapter_trees, &id_remap, &path_to_root_id);
|
||||
|
||||
let mut print_content = String::new();
|
||||
for ChapterTree { tree, .. } in chapter_trees {
|
||||
if !print_content.is_empty() {
|
||||
// Add page break between chapters
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
|
||||
// Add both two CSS properties because of the compatibility issue
|
||||
print_content
|
||||
.push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
|
||||
}
|
||||
serialize(&tree, &mut print_content);
|
||||
}
|
||||
print_content
|
||||
}
|
||||
|
||||
/// Make all IDs unique, and create a map from old to new IDs.
|
||||
///
|
||||
/// The first map is a map of the chapter path to the IDs that were rewritten
|
||||
/// in that chapter (old ID to new ID).
|
||||
///
|
||||
/// The second map is a map of every ID seen to the number of times it has
|
||||
/// been seen. This is used to generate unique IDs.
|
||||
fn make_ids_unique(
|
||||
chapter_trees: &mut [ChapterTree<'_>],
|
||||
) -> (HashMap<PathBuf, HashMap<String, String>>, HashSet<String>) {
|
||||
let mut id_remap = HashMap::new();
|
||||
let mut id_counter = HashSet::new();
|
||||
for ChapterTree {
|
||||
html_path, tree, ..
|
||||
} in chapter_trees
|
||||
{
|
||||
for value in tree.values_mut() {
|
||||
if let Node::Element(el) = value
|
||||
&& let Some(id) = el.attr("id")
|
||||
{
|
||||
let new_id = unique_id(id, &mut id_counter);
|
||||
if new_id != id {
|
||||
let id = id.to_string();
|
||||
el.insert_attr("id", new_id.clone().into());
|
||||
|
||||
let map: &mut HashMap<_, _> = id_remap.entry(html_path.clone()).or_default();
|
||||
map.insert(id, new_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(id_remap, id_counter)
|
||||
}
|
||||
|
||||
/// Generates a map of a chapter path to the ID of the top of the chapter.
|
||||
///
|
||||
/// If a chapter is missing an `h1` tag, then one is synthesized so that the
|
||||
/// print output has something to link to.
|
||||
fn make_root_id_map(
|
||||
chapter_trees: &mut [ChapterTree<'_>],
|
||||
id_counter: &mut HashSet<String>,
|
||||
) -> HashMap<PathBuf, String> {
|
||||
let mut path_to_root_id = HashMap::new();
|
||||
for ChapterTree {
|
||||
chapter,
|
||||
html_path,
|
||||
tree,
|
||||
..
|
||||
} in chapter_trees
|
||||
{
|
||||
let mut h1_found = false;
|
||||
for value in tree.values_mut() {
|
||||
if let Node::Element(el) = value {
|
||||
if el.name() == "h1" {
|
||||
if let Some(id) = el.attr("id") {
|
||||
h1_found = true;
|
||||
path_to_root_id.insert(html_path.clone(), id.to_string());
|
||||
}
|
||||
break;
|
||||
} else if matches!(el.name(), "h2" | "h3" | "h4" | "h5" | "h6") {
|
||||
// h1 not found.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !h1_found {
|
||||
// Synthesize a root id to be able to link to the start of the page.
|
||||
// TODO: This might want to be a warning? Chapters generally
|
||||
// should start with an h1.
|
||||
let mut h1 = Element::new("h1");
|
||||
let id = id_from_content(&chapter.name);
|
||||
let id = unique_id(&id, id_counter);
|
||||
h1.insert_attr("id", id.clone().into());
|
||||
let mut root = tree.root_mut();
|
||||
let mut h1 = root.prepend(Node::Element(h1));
|
||||
let mut a = Element::new("a");
|
||||
a.insert_attr("href", format!("#{id}").into());
|
||||
a.insert_attr("class", "header".into());
|
||||
let mut a = h1.append(Node::Element(a));
|
||||
a.append(Node::Text(chapter.name.clone().into()));
|
||||
path_to_root_id.insert(html_path.clone(), id);
|
||||
}
|
||||
}
|
||||
|
||||
path_to_root_id
|
||||
}
|
||||
|
||||
/// Rewrite links so that they point to IDs on the print page.
|
||||
fn rewrite_links(
|
||||
chapter_trees: &mut [ChapterTree<'_>],
|
||||
id_remap: &HashMap<PathBuf, HashMap<String, String>>,
|
||||
path_to_root_id: &HashMap<PathBuf, String>,
|
||||
) {
|
||||
static_regex!(
|
||||
LINK,
|
||||
r"(?x)
|
||||
(?P<scheme>^[a-z][a-z0-9+.-]*:)?
|
||||
(?P<path>[^\#]+)?
|
||||
(?:\#(?P<anchor>.*))?"
|
||||
);
|
||||
|
||||
// Rewrite path links to go to the appropriate place.
|
||||
for ChapterTree {
|
||||
html_path, tree, ..
|
||||
} in chapter_trees
|
||||
{
|
||||
let base = html_path.parent().expect("path can't be empty");
|
||||
|
||||
for value in tree.values_mut() {
|
||||
let Node::Element(el) = value else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(el.name(), "a" | "img") {
|
||||
continue;
|
||||
}
|
||||
for attr in ["href", "src", "xlink:href"] {
|
||||
let Some(dest) = el.attr(attr) else {
|
||||
continue;
|
||||
};
|
||||
let Some(caps) = LINK.captures(&dest) else {
|
||||
continue;
|
||||
};
|
||||
if caps.name("scheme").is_some() {
|
||||
continue;
|
||||
}
|
||||
// The lookup_key is the key to look up in the remap table.
|
||||
let mut lookup_key = html_path.clone();
|
||||
if let Some(href_path) = caps.name("path")
|
||||
&& let href_path = href_path.as_str()
|
||||
&& !href_path.is_empty()
|
||||
{
|
||||
lookup_key.pop();
|
||||
lookup_key.push(href_path);
|
||||
lookup_key = normalize_path(&lookup_key);
|
||||
let is_a_chapter = path_to_root_id.contains_key(&lookup_key);
|
||||
if !is_a_chapter {
|
||||
// Make the link relative to the print page location.
|
||||
let mut rel_path = normalize_path(&base.join(href_path)).to_url_path();
|
||||
if let Some(anchor) = caps.name("anchor") {
|
||||
rel_path.push('#');
|
||||
rel_path.push_str(anchor.as_str());
|
||||
}
|
||||
el.insert_attr(attr, rel_path.into());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let id = match caps.name("anchor") {
|
||||
Some(anchor_id) => {
|
||||
let anchor_id = anchor_id.as_str().to_string();
|
||||
match id_remap.get(&lookup_key) {
|
||||
Some(id_map) => match id_map.get(&anchor_id) {
|
||||
Some(new_id) => new_id.clone(),
|
||||
None => anchor_id,
|
||||
},
|
||||
None => {
|
||||
// Assume the anchor goes to some non-remapped
|
||||
// ID that already exists.
|
||||
anchor_id
|
||||
}
|
||||
}
|
||||
}
|
||||
None => match path_to_root_id.get(&lookup_key) {
|
||||
Some(id) => id.to_string(),
|
||||
None => {
|
||||
// This should be guaranteed that either the
|
||||
// chapter itself is in the map (for anchor-only
|
||||
// links), or the is_a_chapter check above.
|
||||
panic!(
|
||||
"internal error: expected `{lookup_key:?}` to be in \
|
||||
root map (chapter path is `{html_path:?}`)"
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
el.insert_attr(attr, format!("#{id}").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
crates/mdbook-html/src/html/serialize.rs
Normal file
112
crates/mdbook-html/src/html/serialize.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! Serializes the [`Node`] tree to an HTML string.
|
||||
|
||||
use super::tree::is_void_element;
|
||||
use super::tree::{Element, Node};
|
||||
use ego_tree::{Tree, iter::Edge};
|
||||
use html5ever::{local_name, ns};
|
||||
use mdbook_core::utils::{escape_html, escape_html_attribute};
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Serializes the given tree of [`Node`] elements to an HTML string.
|
||||
pub(crate) fn serialize(tree: &Tree<Node>, output: &mut String) {
|
||||
for edge in tree.root().traverse() {
|
||||
match edge {
|
||||
Edge::Open(node) => match node.value() {
|
||||
Node::Element(el) => serialize_start(el, output),
|
||||
Node::Text(text) => {
|
||||
output.push_str(&escape_html(text));
|
||||
}
|
||||
Node::Comment(comment) => {
|
||||
output.push_str("<!--");
|
||||
output.push_str(comment);
|
||||
output.push_str("-->");
|
||||
}
|
||||
Node::Fragment => {}
|
||||
Node::RawData(html) => {
|
||||
output.push_str(html);
|
||||
}
|
||||
},
|
||||
Edge::Close(node) => {
|
||||
if let Node::Element(el) = node.value() {
|
||||
serialize_end(el, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this HTML element wants a newline to keep the emitted
|
||||
/// output more readable.
|
||||
fn wants_pretty_html_newline(name: &str) -> bool {
|
||||
matches!(name, |"blockquote"| "dd"
|
||||
| "div"
|
||||
| "dl"
|
||||
| "dt"
|
||||
| "h1"
|
||||
| "h2"
|
||||
| "h3"
|
||||
| "h4"
|
||||
| "h5"
|
||||
| "h6"
|
||||
| "hr"
|
||||
| "li"
|
||||
| "ol"
|
||||
| "p"
|
||||
| "pre"
|
||||
| "table"
|
||||
| "tbody"
|
||||
| "thead"
|
||||
| "tr"
|
||||
| "ul")
|
||||
}
|
||||
|
||||
/// Emit the start tag of an element.
|
||||
fn serialize_start(el: &Element, output: &mut String) {
|
||||
let el_name = el.name();
|
||||
if wants_pretty_html_newline(el_name) {
|
||||
if !output.is_empty() {
|
||||
if !output.ends_with('\n') {
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
output.push('<');
|
||||
output.push_str(el_name);
|
||||
for (attr_name, value) in &el.attrs {
|
||||
output.push(' ');
|
||||
match attr_name.ns {
|
||||
ns!() => (),
|
||||
ns!(xml) => output.push_str("xml:"),
|
||||
ns!(xmlns) => {
|
||||
if el.name.local != local_name!("xmlns") {
|
||||
output.push_str("xmlns:");
|
||||
}
|
||||
}
|
||||
ns!(xlink) => output.push_str("xlink:"),
|
||||
_ => (), // TODO what should it do here?
|
||||
}
|
||||
output.push_str(attr_name.local.deref());
|
||||
output.push_str("=\"");
|
||||
output.push_str(&escape_html_attribute(&value));
|
||||
output.push('"');
|
||||
}
|
||||
if el.self_closing {
|
||||
output.push_str(" /");
|
||||
}
|
||||
output.push('>');
|
||||
}
|
||||
|
||||
/// Emit the end tag of an element.
|
||||
fn serialize_end(el: &Element, output: &mut String) {
|
||||
// Void elements do not have an end tag.
|
||||
if el.self_closing || is_void_element(el.name()) {
|
||||
return;
|
||||
}
|
||||
let name = el.name();
|
||||
output.push_str("</");
|
||||
output.push_str(name);
|
||||
output.push('>');
|
||||
if wants_pretty_html_newline(name) {
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
53
crates/mdbook-html/src/html/tests.rs
Normal file
53
crates/mdbook-html/src/html/tests.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::html::tokenizer::parse_html;
|
||||
use html5ever::tokenizer::{Tag, TagKind, Token};
|
||||
|
||||
// Basic tokenizer behavior of a script.
|
||||
#[test]
|
||||
fn parse_html_script() {
|
||||
let script = r#"
|
||||
if (3 < 5 > 10)
|
||||
{
|
||||
alert("The sky is falling!");
|
||||
}
|
||||
"#;
|
||||
let t = format!("<script>{script}</script>");
|
||||
let ts = parse_html(&t);
|
||||
eprintln!("{ts:#?}",);
|
||||
let mut output = String::new();
|
||||
let mut in_script = false;
|
||||
for t in ts {
|
||||
match t {
|
||||
Token::ParseError(e) => panic!("{e:?}"),
|
||||
Token::CharacterTokens(s) => {
|
||||
if in_script {
|
||||
output.push_str(&s)
|
||||
}
|
||||
}
|
||||
Token::TagToken(Tag {
|
||||
kind: TagKind::StartTag,
|
||||
..
|
||||
}) => in_script = true,
|
||||
Token::TagToken(Tag {
|
||||
kind: TagKind::EndTag,
|
||||
..
|
||||
}) => in_script = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
assert_eq!(output, script);
|
||||
}
|
||||
|
||||
// What happens if a script doesn't end.
|
||||
#[test]
|
||||
fn parse_html_script_unclosed() {
|
||||
let t = r#"<script>
|
||||
// Test
|
||||
"#;
|
||||
let ts = parse_html(t);
|
||||
eprintln!("{ts:#?}",);
|
||||
for t in ts {
|
||||
if let Token::ParseError(e) = t {
|
||||
panic!("{e:?}",);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
crates/mdbook-html/src/html/tokenizer.rs
Normal file
83
crates/mdbook-html/src/html/tokenizer.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! Support for parsing HTML.
|
||||
//!
|
||||
//! The primary entry point is [`parse_html`] which uses [`html5ever`] to
|
||||
//! tokenize the input.
|
||||
|
||||
use html5ever::TokenizerResult;
|
||||
use html5ever::tendril::ByteTendril;
|
||||
use html5ever::tokenizer::states::RawKind;
|
||||
use html5ever::tokenizer::{
|
||||
BufferQueue, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer, TokenizerOpts,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
|
||||
/// Collector for HTML tokens.
|
||||
#[derive(Default)]
|
||||
struct TokenCollector {
|
||||
/// Parsed HTML tokens.
|
||||
tokens: RefCell<Vec<Token>>,
|
||||
}
|
||||
|
||||
impl TokenSink for TokenCollector {
|
||||
type Handle = ();
|
||||
|
||||
fn process_token(&self, token: Token, _line_number: u64) -> TokenSinkResult<()> {
|
||||
match &token {
|
||||
Token::DoctypeToken(_) => {}
|
||||
Token::TagToken(tag) => {
|
||||
let tag_name = tag.name.as_bytes();
|
||||
// TODO: This could probably use special support for SVG and MathML.
|
||||
if tag_name == b"script" {
|
||||
match tag.kind {
|
||||
TagKind::StartTag => {
|
||||
self.tokens.borrow_mut().push(token);
|
||||
return TokenSinkResult::RawData(RawKind::ScriptData);
|
||||
}
|
||||
TagKind::EndTag => {}
|
||||
}
|
||||
}
|
||||
if tag_name == b"style" {
|
||||
match tag.kind {
|
||||
TagKind::StartTag => {
|
||||
self.tokens.borrow_mut().push(token);
|
||||
return TokenSinkResult::RawData(RawKind::Rawtext);
|
||||
}
|
||||
TagKind::EndTag => {}
|
||||
}
|
||||
}
|
||||
self.tokens.borrow_mut().push(token);
|
||||
}
|
||||
Token::CommentToken(_) => {
|
||||
self.tokens.borrow_mut().push(token);
|
||||
}
|
||||
Token::CharacterTokens(_) => {
|
||||
self.tokens.borrow_mut().push(token);
|
||||
}
|
||||
Token::NullCharacterToken => {}
|
||||
Token::EOFToken => {}
|
||||
Token::ParseError(_) => {
|
||||
self.tokens.borrow_mut().push(token);
|
||||
}
|
||||
}
|
||||
TokenSinkResult::Continue
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse HTML into tokens.
|
||||
pub(crate) fn parse_html(html: &str) -> Vec<Token> {
|
||||
let tendril: ByteTendril = html.as_bytes().into();
|
||||
let mut queue = BufferQueue::default();
|
||||
queue.push_back(tendril.try_reinterpret().unwrap());
|
||||
|
||||
let collector = TokenCollector::default();
|
||||
let tok = Tokenizer::new(collector, TokenizerOpts::default());
|
||||
let result = tok.feed(&mut queue);
|
||||
assert_eq!(result, TokenizerResult::Done);
|
||||
assert!(
|
||||
queue.is_empty(),
|
||||
"queue wasn't empty: {:?}",
|
||||
queue.pop_front()
|
||||
);
|
||||
tok.end();
|
||||
tok.sink.tokens.take()
|
||||
}
|
||||
1155
crates/mdbook-html/src/html/tree.rs
Normal file
1155
crates/mdbook-html/src/html/tree.rs
Normal file
File diff suppressed because it is too large
Load Diff
696
crates/mdbook-html/src/html_handlebars/hbs_renderer.rs
Normal file
696
crates/mdbook-html/src/html_handlebars/hbs_renderer.rs
Normal file
@@ -0,0 +1,696 @@
|
||||
use super::helpers;
|
||||
use super::static_files::StaticFiles;
|
||||
use crate::html::ChapterTree;
|
||||
use crate::html::{build_trees, render_markdown, serialize};
|
||||
use crate::theme::Theme;
|
||||
use crate::utils::ToUrlPath;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use handlebars::Handlebars;
|
||||
use mdbook_core::book::{Book, BookItem, Chapter};
|
||||
use mdbook_core::config::{BookConfig, Config, HtmlConfig};
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_renderer::{RenderContext, Renderer};
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::error;
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
/// The HTML renderer for mdBook.
|
||||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct HtmlHandlebars;
|
||||
|
||||
impl HtmlHandlebars {
|
||||
/// Returns a new instance of [`HtmlHandlebars`].
|
||||
pub fn new() -> Self {
|
||||
HtmlHandlebars
|
||||
}
|
||||
|
||||
fn render_chapter(
|
||||
&self,
|
||||
chapter_tree: &ChapterTree<'_>,
|
||||
prev_ch: Option<&Chapter>,
|
||||
next_ch: Option<&Chapter>,
|
||||
mut ctx: RenderChapterContext<'_>,
|
||||
) -> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
let ch = chapter_tree.chapter;
|
||||
|
||||
let path = ch.path.as_ref().unwrap();
|
||||
// "print.html" is used for the print page.
|
||||
if path == Path::new("print.md") {
|
||||
bail!("{} is reserved for internal use", path.display());
|
||||
};
|
||||
|
||||
if let Some(ref edit_url_template) = ctx.html_config.edit_url_template {
|
||||
let full_path = ctx.book_config.src.to_str().unwrap_or_default().to_owned()
|
||||
+ "/"
|
||||
+ ch.source_path
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
let edit_url = edit_url_template.replace("{path}", &full_path);
|
||||
ctx.data
|
||||
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
|
||||
}
|
||||
|
||||
let mut content = String::new();
|
||||
serialize(&chapter_tree.tree, &mut content);
|
||||
|
||||
let ctx_path = path
|
||||
.to_str()
|
||||
.with_context(|| "Could not convert path to str")?;
|
||||
let filepath = Path::new(&ctx_path).with_extension("html");
|
||||
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
|
||||
let title = if let Some(title) = ctx.chapter_titles.get(path) {
|
||||
title.clone()
|
||||
} else if book_title.is_empty() {
|
||||
ch.name.clone()
|
||||
} else {
|
||||
ch.name.clone() + " - " + book_title
|
||||
};
|
||||
|
||||
ctx.data.insert("path".to_owned(), json!(path));
|
||||
ctx.data.insert("content".to_owned(), json!(content));
|
||||
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data
|
||||
.insert("path_to_root".to_owned(), json!(fs::path_to_root(path)));
|
||||
if let Some(ref section) = ch.number {
|
||||
ctx.data
|
||||
.insert("section".to_owned(), json!(section.to_string()));
|
||||
}
|
||||
|
||||
let redirects = collect_redirects_for_path(&filepath, &ctx.html_config.redirect)?;
|
||||
if !redirects.is_empty() {
|
||||
ctx.data.insert(
|
||||
"fragment_map".to_owned(),
|
||||
json!(serde_json::to_string(&redirects)?),
|
||||
);
|
||||
}
|
||||
|
||||
let mut nav = |name: &str, ch: Option<&Chapter>| {
|
||||
let Some(ch) = ch else { return };
|
||||
let path = ch
|
||||
.path
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.with_extension("html")
|
||||
.to_url_path();
|
||||
let obj = json!( {
|
||||
"title": ch.name,
|
||||
"link": path,
|
||||
});
|
||||
ctx.data.insert(name.to_string(), obj);
|
||||
};
|
||||
nav("previous", prev_ch);
|
||||
nav("next", next_ch);
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
// Write to file
|
||||
let out_path = ctx.destination.join(filepath);
|
||||
fs::write(&out_path, rendered)?;
|
||||
|
||||
if prev_ch.is_none() {
|
||||
ctx.data.insert("path".to_owned(), json!("index.md"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
ctx.data.insert("is_index".to_owned(), json!(true));
|
||||
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
|
||||
debug!("Creating index.html from {}", ctx_path);
|
||||
fs::write(ctx.destination.join("index.html"), rendered_index)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_404(
|
||||
&self,
|
||||
ctx: &RenderContext,
|
||||
html_config: &HtmlConfig,
|
||||
src_dir: &Path,
|
||||
handlebars: &mut Handlebars<'_>,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
) -> Result<()> {
|
||||
let content_404 = if let Some(ref filename) = html_config.input_404 {
|
||||
let path = src_dir.join(filename);
|
||||
fs::read_to_string(&path).with_context(|| "failed to read the 404 input file")?
|
||||
} 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() {
|
||||
fs::read_to_string(&default_404_location)
|
||||
.with_context(|| "failed to read the 404 input file")?
|
||||
} else {
|
||||
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
|
||||
navigation bar or search to continue."
|
||||
.to_string()
|
||||
}
|
||||
};
|
||||
let options = crate::html::HtmlRenderOptions::new(
|
||||
Path::new("404.md"),
|
||||
html_config,
|
||||
ctx.config.rust.edition,
|
||||
);
|
||||
let html_content_404 = render_markdown(&content_404, &options);
|
||||
|
||||
let mut data_404 = data.clone();
|
||||
let base_url = if let Some(site_url) = &html_config.site_url {
|
||||
site_url
|
||||
} else {
|
||||
debug!(
|
||||
"HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
|
||||
this to ensure the 404 page work correctly, especially if your site is hosted in a \
|
||||
subdirectory on the HTTP server."
|
||||
);
|
||||
"/"
|
||||
};
|
||||
data_404.insert("base_url".to_owned(), json!(base_url));
|
||||
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
|
||||
data_404.insert("path".to_owned(), json!("404.md"));
|
||||
data_404.insert("content".to_owned(), json!(html_content_404));
|
||||
|
||||
let mut title = String::from("Page not found");
|
||||
if let Some(book_title) = &ctx.config.book.title {
|
||||
title.push_str(" - ");
|
||||
title.push_str(book_title);
|
||||
}
|
||||
data_404.insert("title".to_owned(), json!(title));
|
||||
let rendered = handlebars.render("index", &data_404)?;
|
||||
|
||||
let output_file = ctx.destination.join(html_config.get_404_output_file());
|
||||
fs::write(output_file, rendered)?;
|
||||
debug!("Creating 404.html ✓");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_print_page(
|
||||
&self,
|
||||
ctx: &RenderContext,
|
||||
handlebars: &Handlebars<'_>,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
chapter_trees: Vec<ChapterTree<'_>>,
|
||||
) -> Result<String> {
|
||||
let print_content = crate::html::render_print_page(chapter_trees);
|
||||
|
||||
if let Some(ref title) = ctx.config.book.title {
|
||||
data.insert("title".to_owned(), json!(title));
|
||||
} else {
|
||||
// Make sure that the Print chapter does not display the title from
|
||||
// the last rendered chapter by removing it from its context
|
||||
data.remove("title");
|
||||
}
|
||||
data.insert("is_print".to_owned(), json!(true));
|
||||
data.insert("path".to_owned(), json!("print.md"));
|
||||
data.insert("content".to_owned(), json!(print_content));
|
||||
data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(fs::path_to_root(Path::new("print.md"))),
|
||||
);
|
||||
|
||||
debug!("Render template");
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) {
|
||||
handlebars.register_helper(
|
||||
"toc",
|
||||
Box::new(helpers::toc::RenderToc {
|
||||
no_section_label: html_config.no_section_label,
|
||||
}),
|
||||
);
|
||||
handlebars.register_helper("fa", Box::new(helpers::fontawesome::fa_helper));
|
||||
}
|
||||
|
||||
fn emit_redirects(
|
||||
&self,
|
||||
root: &Path,
|
||||
handlebars: &Handlebars<'_>,
|
||||
redirects: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
if redirects.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!("Emitting redirects");
|
||||
let redirects = combine_fragment_redirects(redirects);
|
||||
|
||||
for (original, (dest, fragment_map)) in redirects {
|
||||
// Note: all paths are relative to the build directory, so the
|
||||
// leading slash in an absolute path means nothing (and would mess
|
||||
// up `root.join(original)`).
|
||||
let original = original.trim_start_matches('/');
|
||||
let filename = root.join(original);
|
||||
if filename.exists() {
|
||||
// This redirect is handled by the in-page fragment mapper.
|
||||
continue;
|
||||
}
|
||||
if dest.is_empty() {
|
||||
bail!(
|
||||
"redirect entry for `{original}` only has source paths with `#` fragments\n\
|
||||
There must be an entry without the `#` fragment to determine the default \
|
||||
destination."
|
||||
);
|
||||
}
|
||||
debug!("Redirecting \"{}\" → \"{}\"", original, dest);
|
||||
self.emit_redirect(handlebars, &filename, &dest, &fragment_map)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirect(
|
||||
&self,
|
||||
handlebars: &Handlebars<'_>,
|
||||
original: &Path,
|
||||
destination: &str,
|
||||
fragment_map: &BTreeMap<String, String>,
|
||||
) -> Result<()> {
|
||||
if let Some(parent) = original.parent() {
|
||||
fs::create_dir_all(parent)?
|
||||
}
|
||||
|
||||
let js_map = serde_json::to_string(fragment_map)?;
|
||||
|
||||
let ctx = json!({
|
||||
"fragment_map": js_map,
|
||||
"url": destination,
|
||||
});
|
||||
let rendered = handlebars.render("redirect", &ctx).with_context(|| {
|
||||
format!(
|
||||
"Unable to create a redirect file at `{}`",
|
||||
original.display()
|
||||
)
|
||||
})?;
|
||||
fs::write(original, rendered)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn name(&self) -> &str {
|
||||
"html"
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
let book_config = &ctx.config.book;
|
||||
let html_config = ctx.config.html_config().unwrap_or_default();
|
||||
let src_dir = ctx.root.join(&ctx.config.book.src);
|
||||
let destination = &ctx.destination;
|
||||
let book = &ctx.book;
|
||||
let build_dir = ctx.root.join(&ctx.config.build.build_dir);
|
||||
|
||||
if destination.exists() {
|
||||
fs::remove_dir_content(destination)
|
||||
.with_context(|| "Unable to remove stale HTML output")?;
|
||||
}
|
||||
|
||||
trace!("render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
let theme_dir = match html_config.theme {
|
||||
Some(ref theme) => {
|
||||
let dir = ctx.root.join(theme);
|
||||
if !dir.is_dir() {
|
||||
bail!("theme dir {} does not exist", dir.display());
|
||||
}
|
||||
dir
|
||||
}
|
||||
None => ctx.root.join("theme"),
|
||||
};
|
||||
|
||||
let theme = Theme::new(theme_dir);
|
||||
|
||||
debug!("Register the index handlebars template");
|
||||
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
|
||||
|
||||
debug!("Register the head handlebars template");
|
||||
handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?;
|
||||
|
||||
debug!("Register the redirect handlebars template");
|
||||
handlebars
|
||||
.register_template_string("redirect", String::from_utf8(theme.redirect.clone())?)?;
|
||||
|
||||
debug!("Register the header handlebars template");
|
||||
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
|
||||
|
||||
debug!("Register 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);
|
||||
|
||||
let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?;
|
||||
|
||||
let chapter_trees = build_trees(book, &html_config, ctx.config.rust.edition);
|
||||
|
||||
fs::create_dir_all(destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
let mut static_files = StaticFiles::new(&theme, &html_config, &ctx.root)?;
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
{
|
||||
let default = mdbook_core::config::Search::default();
|
||||
let search = html_config.search.as_ref().unwrap_or(&default);
|
||||
if search.enable {
|
||||
super::search::create_files(&search, &mut static_files, &chapter_trees)?;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Render toc js");
|
||||
{
|
||||
let rendered_toc = handlebars.render("toc_js", &data)?;
|
||||
static_files.add_builtin("toc.js", rendered_toc.as_bytes());
|
||||
debug!("Creating toc.js ✓");
|
||||
}
|
||||
|
||||
if html_config.hash_files {
|
||||
static_files.hash_files()?;
|
||||
}
|
||||
|
||||
debug!("Copy static files");
|
||||
let resource_helper = static_files
|
||||
.write_files(&destination)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
|
||||
handlebars.register_helper("resource", Box::new(resource_helper));
|
||||
|
||||
debug!("Render toc html");
|
||||
{
|
||||
data.insert("is_toc_html".to_owned(), json!(true));
|
||||
data.insert("path".to_owned(), json!("toc.html"));
|
||||
let rendered_toc = handlebars.render("toc_html", &data)?;
|
||||
fs::write(destination.join("toc.html"), rendered_toc)?;
|
||||
debug!("Creating toc.html ✓");
|
||||
data.remove("path");
|
||||
data.remove("is_toc_html");
|
||||
}
|
||||
|
||||
fs::write(
|
||||
destination.join(".nojekyll"),
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
fs::write(destination.join("CNAME"), format!("{cname}\n"))?;
|
||||
}
|
||||
|
||||
for (i, chapter_tree) in chapter_trees.iter().enumerate() {
|
||||
let previous = (i != 0).then(|| chapter_trees[i - 1].chapter);
|
||||
let next = (i != chapter_trees.len() - 1).then(|| chapter_trees[i + 1].chapter);
|
||||
let ctx = RenderChapterContext {
|
||||
handlebars: &handlebars,
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
book_config: book_config.clone(),
|
||||
html_config: html_config.clone(),
|
||||
chapter_titles: &ctx.chapter_titles,
|
||||
};
|
||||
self.render_chapter(chapter_tree, previous, next, ctx)?;
|
||||
}
|
||||
|
||||
// Render 404 page
|
||||
if html_config.input_404 != Some("".to_string()) {
|
||||
self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?;
|
||||
}
|
||||
|
||||
// Render the print version.
|
||||
if html_config.print.enable {
|
||||
let print_rendered =
|
||||
self.render_print_page(ctx, &handlebars, &mut data, chapter_trees)?;
|
||||
|
||||
fs::write(destination.join("print.html"), print_rendered)?;
|
||||
debug!("Creating print.html ✓");
|
||||
}
|
||||
|
||||
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
|
||||
.context("Unable to emit redirects")?;
|
||||
|
||||
// Copy all remaining files, avoid a recursive copy from/to the book build dir
|
||||
fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?;
|
||||
|
||||
info!("HTML book written to `{}`", destination.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(
|
||||
root: &Path,
|
||||
book: &Book,
|
||||
config: &Config,
|
||||
html_config: &HtmlConfig,
|
||||
theme: &Theme,
|
||||
) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
trace!("make_data");
|
||||
|
||||
let mut data = serde_json::Map::new();
|
||||
data.insert(
|
||||
"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()),
|
||||
);
|
||||
data.insert(
|
||||
"description".to_owned(),
|
||||
json!(config.book.description.clone().unwrap_or_default()),
|
||||
);
|
||||
if theme.favicon_png.is_some() {
|
||||
data.insert("favicon_png".to_owned(), json!("favicon.png"));
|
||||
}
|
||||
if theme.favicon_svg.is_some() {
|
||||
data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
|
||||
}
|
||||
if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint {
|
||||
data.insert(
|
||||
"live_reload_endpoint".to_owned(),
|
||||
json!(live_reload_endpoint),
|
||||
);
|
||||
}
|
||||
|
||||
let default_theme = match html_config.default_theme {
|
||||
Some(ref theme) => theme.to_lowercase(),
|
||||
None => "light".to_string(),
|
||||
};
|
||||
data.insert("default_theme".to_owned(), json!(default_theme));
|
||||
|
||||
let preferred_dark_theme = match html_config.preferred_dark_theme {
|
||||
Some(ref theme) => theme.to_lowercase(),
|
||||
None => "navy".to_string(),
|
||||
};
|
||||
data.insert(
|
||||
"preferred_dark_theme".to_owned(),
|
||||
json!(preferred_dark_theme),
|
||||
);
|
||||
|
||||
if html_config.mathjax_support {
|
||||
data.insert("mathjax_support".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional style
|
||||
if !html_config.additional_css.is_empty() {
|
||||
let mut css = Vec::new();
|
||||
for style in &html_config.additional_css {
|
||||
match style.strip_prefix(root) {
|
||||
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
|
||||
}
|
||||
}
|
||||
data.insert("additional_css".to_owned(), json!(css));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional script
|
||||
if !html_config.additional_js.is_empty() {
|
||||
let mut js = Vec::new();
|
||||
for script in &html_config.additional_js {
|
||||
match script.strip_prefix(root) {
|
||||
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => js.push(script.to_str().expect("Could not convert to str")),
|
||||
}
|
||||
}
|
||||
data.insert("additional_js".to_owned(), json!(js));
|
||||
}
|
||||
|
||||
if html_config.playground.editable && html_config.playground.copy_js {
|
||||
data.insert("playground_js".to_owned(), json!(true));
|
||||
if html_config.playground.line_numbers {
|
||||
data.insert("playground_line_numbers".to_owned(), json!(true));
|
||||
}
|
||||
}
|
||||
if html_config.playground.copyable {
|
||||
data.insert("playground_copyable".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
data.insert("print_enable".to_owned(), json!(html_config.print.enable));
|
||||
data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
|
||||
data.insert("fold_level".to_owned(), json!(html_config.fold.level));
|
||||
data.insert(
|
||||
"sidebar_header_nav".to_owned(),
|
||||
json!(html_config.sidebar_header_nav),
|
||||
);
|
||||
|
||||
let search = html_config.search.clone();
|
||||
if cfg!(feature = "search") {
|
||||
let search = search.unwrap_or_default();
|
||||
data.insert("search_enabled".to_owned(), json!(search.enable));
|
||||
data.insert(
|
||||
"search_js".to_owned(),
|
||||
json!(search.enable && search.copy_js),
|
||||
);
|
||||
} else if search.is_some() {
|
||||
warn!("mdBook compiled without search support, ignoring `output.html.search` table");
|
||||
warn!(
|
||||
"please reinstall with `cargo install mdbook --force --features search`to use the \
|
||||
search feature"
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(ref git_repository_url) = html_config.git_repository_url {
|
||||
data.insert("git_repository_url".to_owned(), json!(git_repository_url));
|
||||
}
|
||||
|
||||
let git_repository_icon = match html_config.git_repository_icon {
|
||||
Some(ref git_repository_icon) => git_repository_icon,
|
||||
None => "fab-github",
|
||||
};
|
||||
let git_repository_icon_class = match git_repository_icon.split('-').next() {
|
||||
Some("fa") => "regular",
|
||||
Some("fas") => "solid",
|
||||
Some("fab") => "brands",
|
||||
_ => "regular",
|
||||
};
|
||||
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
|
||||
data.insert(
|
||||
"git_repository_icon_class".to_owned(),
|
||||
json!(git_repository_icon_class),
|
||||
);
|
||||
|
||||
let mut chapters = vec![];
|
||||
|
||||
for item in book.iter() {
|
||||
// Create the data to inject in the template
|
||||
let mut chapter = BTreeMap::new();
|
||||
|
||||
match *item {
|
||||
BookItem::PartTitle(ref title) => {
|
||||
chapter.insert("part".to_owned(), json!(title));
|
||||
}
|
||||
BookItem::Chapter(ref ch) => {
|
||||
if let Some(ref section) = ch.number {
|
||||
chapter.insert("section".to_owned(), json!(section.to_string()));
|
||||
}
|
||||
|
||||
chapter.insert(
|
||||
"has_sub_items".to_owned(),
|
||||
json!((!ch.sub_items.is_empty()).to_string()),
|
||||
);
|
||||
|
||||
chapter.insert("name".to_owned(), json!(ch.name));
|
||||
if let Some(ref path) = ch.path {
|
||||
let p = path
|
||||
.to_str()
|
||||
.with_context(|| "Could not convert path to str")?;
|
||||
chapter.insert("path".to_owned(), json!(p));
|
||||
}
|
||||
}
|
||||
BookItem::Separator => {
|
||||
chapter.insert("spacer".to_owned(), json!("_spacer_"));
|
||||
}
|
||||
}
|
||||
|
||||
chapters.push(chapter);
|
||||
}
|
||||
|
||||
data.insert("chapters".to_owned(), json!(chapters));
|
||||
|
||||
debug!("[*]: JSON constructed");
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
struct RenderChapterContext<'a> {
|
||||
handlebars: &'a Handlebars<'a>,
|
||||
destination: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
book_config: BookConfig,
|
||||
html_config: HtmlConfig,
|
||||
chapter_titles: &'a HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
/// Redirect mapping.
|
||||
///
|
||||
/// The key is the source path (like `foo/bar.html`). The value is a tuple
|
||||
/// `(destination_path, fragment_map)`. The `destination_path` is the page to
|
||||
/// redirect to. `fragment_map` is the map of fragments that override the
|
||||
/// destination. For example, a fragment `#foo` could redirect to any other
|
||||
/// page or site.
|
||||
type CombinedRedirects = BTreeMap<String, (String, BTreeMap<String, String>)>;
|
||||
fn combine_fragment_redirects(redirects: &HashMap<String, String>) -> CombinedRedirects {
|
||||
let mut combined: CombinedRedirects = BTreeMap::new();
|
||||
// This needs to extract the fragments to generate the fragment map.
|
||||
for (original, new) in redirects {
|
||||
if let Some((source_path, source_fragment)) = original.rsplit_once('#') {
|
||||
let e = combined.entry(source_path.to_string()).or_default();
|
||||
if let Some(old) = e.1.insert(format!("#{source_fragment}"), new.clone()) {
|
||||
error!(
|
||||
"internal error: found duplicate fragment redirect \
|
||||
{old} for {source_path}#{source_fragment}"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let e = combined.entry(original.to_string()).or_default();
|
||||
e.0 = new.clone();
|
||||
}
|
||||
}
|
||||
combined
|
||||
}
|
||||
|
||||
/// Collects fragment redirects for an existing page.
|
||||
///
|
||||
/// The returned map has keys like `#foo` and the value is the new destination
|
||||
/// path or URL.
|
||||
fn collect_redirects_for_path(
|
||||
path: &Path,
|
||||
redirects: &HashMap<String, String>,
|
||||
) -> Result<BTreeMap<String, String>> {
|
||||
let path = format!("/{}", path.to_url_path());
|
||||
if redirects.contains_key(&path) {
|
||||
bail!(
|
||||
"redirect found for existing chapter at `{path}`\n\
|
||||
Either delete the redirect or remove the chapter."
|
||||
);
|
||||
}
|
||||
|
||||
let key_prefix = format!("{path}#");
|
||||
let map = redirects
|
||||
.iter()
|
||||
.filter_map(|(source, dest)| {
|
||||
source
|
||||
.strip_prefix(&key_prefix)
|
||||
.map(|fragment| (format!("#{fragment}"), dest.to_string()))
|
||||
})
|
||||
.collect();
|
||||
Ok(map)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use font_awesome_as_a_crate as fa;
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use tracing::trace;
|
||||
|
||||
pub(crate) fn fa_helper(
|
||||
h: &Helper<'_>,
|
||||
_r: &Handlebars<'_>,
|
||||
_ctx: &Context,
|
||||
_rc: &mut RenderContext<'_, '_>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("fa_helper (handlebars helper)");
|
||||
|
||||
let type_ = h
|
||||
.param(0)
|
||||
.and_then(|v| v.value().as_str())
|
||||
.and_then(|v| fa::Type::from_str(v).ok())
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other(
|
||||
"Param 0 with String type is required for fontawesome helper.".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let name = h.param(1).and_then(|v| v.value().as_str()).ok_or_else(|| {
|
||||
RenderErrorReason::Other(
|
||||
"Param 1 with String type is required for fontawesome helper.".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
trace!("fa_helper: {} {}", type_, name);
|
||||
|
||||
let name = name
|
||||
.strip_prefix("fa-")
|
||||
.or_else(|| name.strip_prefix("fab-"))
|
||||
.or_else(|| name.strip_prefix("fas-"))
|
||||
.unwrap_or(name);
|
||||
|
||||
if let Some(id) = h.param(2).and_then(|v| v.value().as_str()) {
|
||||
out.write(&format!("<span class=fa-svg id=\"{}\">", id))?;
|
||||
} else {
|
||||
out.write("<span class=fa-svg>")?;
|
||||
}
|
||||
out.write(
|
||||
fa::svg(type_, name)
|
||||
.map_err(|_| RenderErrorReason::Other(format!("Missing font {}", name)))?,
|
||||
)?;
|
||||
out.write("</span>")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
3
crates/mdbook-html/src/html_handlebars/helpers/mod.rs
Normal file
3
crates/mdbook-html/src/html_handlebars/helpers/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub(crate) mod fontawesome;
|
||||
pub(crate) mod resources;
|
||||
pub(crate) mod toc;
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::utils;
|
||||
use mdbook_core::utils;
|
||||
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
@@ -8,7 +8,7 @@ use handlebars::{
|
||||
|
||||
// Handlebars helper to find filenames with hashes in them
|
||||
#[derive(Clone)]
|
||||
pub struct ResourceHelper {
|
||||
pub(crate) struct ResourceHelper {
|
||||
pub hash_map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ impl HelperDef for ResourceHelper {
|
||||
) -> Result<(), RenderError> {
|
||||
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
|
||||
RenderErrorReason::Other(
|
||||
"Param 0 with String type is required for theme_option helper.".to_owned(),
|
||||
"Param 0 with String type is required for resource helper.".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use std::path::Path;
|
||||
use std::{cmp::Ordering, collections::BTreeMap};
|
||||
|
||||
use crate::utils::special_escape;
|
||||
|
||||
use crate::utils::ToUrlPath;
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
};
|
||||
use mdbook_core::utils::escape_html_attribute;
|
||||
use std::path::Path;
|
||||
use std::{cmp::Ordering, collections::BTreeMap};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RenderToc {
|
||||
pub(crate) struct RenderToc {
|
||||
pub no_section_label: bool,
|
||||
}
|
||||
|
||||
@@ -58,13 +57,13 @@ impl HelperDef for RenderToc {
|
||||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
let mut first = true;
|
||||
|
||||
for item in chapters {
|
||||
let (_section, level) = if let Some(s) = item.get("section") {
|
||||
(s.as_str(), s.matches('.').count())
|
||||
} else {
|
||||
("", 1)
|
||||
};
|
||||
let level = item
|
||||
.get("section")
|
||||
.map(|s| s.matches('.').count())
|
||||
.unwrap_or(1);
|
||||
|
||||
// Expand if folding is disabled, or if levels that are larger than this would not
|
||||
// be folded.
|
||||
@@ -72,25 +71,31 @@ impl HelperDef for RenderToc {
|
||||
|
||||
match level.cmp(¤t_level) {
|
||||
Ordering::Greater => {
|
||||
while level > current_level {
|
||||
out.write("<li>")?;
|
||||
out.write("<ol class=\"section\">")?;
|
||||
current_level += 1;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
// There is an assumption that when descending, it can
|
||||
// only go one level down at a time. This should be
|
||||
// enforced by the nature of markdown lists and the
|
||||
// summary parser.
|
||||
assert_eq!(level, current_level + 1);
|
||||
current_level += 1;
|
||||
out.write("<ol class=\"section\">")?;
|
||||
write_li_open_tag(out, is_expanded)?;
|
||||
}
|
||||
Ordering::Less => {
|
||||
while level < current_level {
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
out.write("</ol>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
write_li_open_tag(out, is_expanded)?;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
write_li_open_tag(out, is_expanded, !item.contains_key("section"))?;
|
||||
if !first {
|
||||
out.write("</li>")?;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded)?;
|
||||
}
|
||||
}
|
||||
first = false;
|
||||
|
||||
// Spacer
|
||||
if item.contains_key("spacer") {
|
||||
@@ -101,21 +106,18 @@ impl HelperDef for RenderToc {
|
||||
// Part title
|
||||
if let Some(title) = item.get("part") {
|
||||
out.write("<li class=\"part-title\">")?;
|
||||
out.write(&special_escape(title))?;
|
||||
out.write(&escape_html_attribute(title))?;
|
||||
out.write("</li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
out.write("<span class=\"chapter-link-wrapper\">")?;
|
||||
|
||||
// Link
|
||||
let path_exists = 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(path).with_extension("html").to_url_path();
|
||||
|
||||
// Add link
|
||||
out.write(&tmp)?;
|
||||
@@ -127,7 +129,7 @@ impl HelperDef for RenderToc {
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
out.write("<div>")?;
|
||||
out.write("<span>")?;
|
||||
false
|
||||
}
|
||||
};
|
||||
@@ -142,47 +144,41 @@ impl HelperDef for RenderToc {
|
||||
}
|
||||
|
||||
if let Some(name) = item.get("name") {
|
||||
out.write(&special_escape(name))?
|
||||
out.write(&escape_html_attribute(name))?;
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
out.write("</a>")?;
|
||||
} else {
|
||||
out.write("</div>")?;
|
||||
out.write("</span>")?;
|
||||
}
|
||||
|
||||
// Render expand/collapse toggle
|
||||
if let Some(flag) = item.get("has_sub_items") {
|
||||
let has_sub_items = flag.parse::<bool>().unwrap_or_default();
|
||||
if fold_enable && has_sub_items {
|
||||
out.write("<a class=\"toggle\"><div>❱</div></a>")?;
|
||||
// The <div> here is to manage rotating the element when
|
||||
// the chapter title is long and word-wraps.
|
||||
out.write("<a class=\"chapter-fold-toggle\"><div>❱</div></a>")?;
|
||||
}
|
||||
}
|
||||
out.write("</li>")?;
|
||||
out.write("</span>")?;
|
||||
}
|
||||
while current_level > 1 {
|
||||
out.write("</ol>")?;
|
||||
while current_level > 0 {
|
||||
out.write("</li>")?;
|
||||
out.write("</ol>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
|
||||
out.write("</ol>")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_li_open_tag(
|
||||
out: &mut dyn Output,
|
||||
is_expanded: bool,
|
||||
is_affix: bool,
|
||||
) -> Result<(), std::io::Error> {
|
||||
fn write_li_open_tag(out: &mut dyn Output, is_expanded: bool) -> Result<(), std::io::Error> {
|
||||
let mut li = String::from("<li class=\"chapter-item ");
|
||||
if is_expanded {
|
||||
li.push_str("expanded ");
|
||||
}
|
||||
if is_affix {
|
||||
li.push_str("affix ");
|
||||
}
|
||||
li.push_str("\">");
|
||||
out.write(&li)
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
pub use self::hbs_renderer::HtmlHandlebars;
|
||||
pub use self::static_files::StaticFiles;
|
||||
|
||||
mod hbs_renderer;
|
||||
mod helpers;
|
||||
mod static_files;
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
mod search;
|
||||
mod static_files;
|
||||
|
||||
pub use self::hbs_renderer::HtmlHandlebars;
|
||||
@@ -1,19 +1,18 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use elasticlunr::{Index, IndexBuilder};
|
||||
use pulldown_cmark::*;
|
||||
|
||||
use crate::book::{Book, BookItem, Chapter};
|
||||
use crate::config::{Search, SearchChapterSettings};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::StaticFiles;
|
||||
use super::static_files::StaticFiles;
|
||||
use crate::html::{ChapterTree, Node};
|
||||
use crate::theme::searcher;
|
||||
use crate::utils;
|
||||
use log::{debug, warn};
|
||||
use crate::utils::ToUrlPath;
|
||||
use anyhow::{Result, bail};
|
||||
use ego_tree::iter::Edge;
|
||||
use elasticlunr::{Index, IndexBuilder};
|
||||
use mdbook_core::book::Chapter;
|
||||
use mdbook_core::config::{Search, SearchChapterSettings};
|
||||
use mdbook_core::static_regex;
|
||||
use serde::Serialize;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
const MAX_WORD_LENGTH_TO_INDEX: usize = 80;
|
||||
|
||||
@@ -27,10 +26,10 @@ fn tokenize(text: &str) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// Creates all files required for search.
|
||||
pub fn create_files(
|
||||
pub(super) fn create_files(
|
||||
search_config: &Search,
|
||||
static_files: &mut StaticFiles,
|
||||
book: &Book,
|
||||
chapter_trees: &[ChapterTree<'_>],
|
||||
) -> Result<()> {
|
||||
let mut index = IndexBuilder::new()
|
||||
.add_field_with_tokenizer("title", Box::new(&tokenize))
|
||||
@@ -38,23 +37,19 @@ pub fn create_files(
|
||||
.add_field_with_tokenizer("breadcrumbs", Box::new(&tokenize))
|
||||
.build();
|
||||
|
||||
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
||||
// These are links to all of the headings in all of the chapters.
|
||||
let mut doc_urls = Vec::new();
|
||||
|
||||
let chapter_configs = sort_search_config(&search_config.chapter);
|
||||
validate_chapter_config(&chapter_configs, book)?;
|
||||
validate_chapter_config(&chapter_configs, chapter_trees)?;
|
||||
|
||||
for item in book.iter() {
|
||||
let chapter = match item {
|
||||
BookItem::Chapter(ch) if !ch.is_draft_chapter() => ch,
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(path) = settings_path(chapter) {
|
||||
let chapter_settings = get_chapter_settings(&chapter_configs, path);
|
||||
if !chapter_settings.enable.unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
for ct in chapter_trees {
|
||||
let path = settings_path(ct.chapter);
|
||||
let chapter_settings = get_chapter_settings(&chapter_configs, path);
|
||||
if !chapter_settings.enable.unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
render_item(&mut index, search_config, &mut doc_urls, chapter)?;
|
||||
index_chapter(&mut index, search_config, &mut doc_urls, ct)?;
|
||||
}
|
||||
|
||||
let index = write_to_json(index, search_config, doc_urls)?;
|
||||
@@ -88,145 +83,110 @@ fn add_doc(
|
||||
index: &mut Index,
|
||||
doc_urls: &mut Vec<String>,
|
||||
anchor_base: &str,
|
||||
heading: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
section_id: &Option<CowStr<'_>>,
|
||||
heading_id: &str,
|
||||
items: &[&str],
|
||||
) {
|
||||
// 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 mut url = anchor_base.to_string();
|
||||
if !heading_id.is_empty() {
|
||||
url.push('#');
|
||||
url.push_str(heading_id);
|
||||
}
|
||||
|
||||
let url = if let Some(id) = section_id {
|
||||
Cow::Owned(format!("{anchor_base}#{id}"))
|
||||
} else {
|
||||
Cow::Borrowed(anchor_base)
|
||||
};
|
||||
let url = utils::collapse_whitespace(url.trim());
|
||||
let doc_ref = doc_urls.len().to_string();
|
||||
doc_urls.push(url.into());
|
||||
doc_urls.push(url);
|
||||
|
||||
let items = items.iter().map(|&x| utils::collapse_whitespace(x.trim()));
|
||||
let items = items.iter().map(|&x| collapse_whitespace(x.trim()));
|
||||
index.add_doc(&doc_ref, items);
|
||||
}
|
||||
|
||||
/// Renders markdown into flat unformatted text and adds it to the search index.
|
||||
fn render_item(
|
||||
/// Adds the chapter to the search index.
|
||||
fn index_chapter(
|
||||
index: &mut Index,
|
||||
search_config: &Search,
|
||||
doc_urls: &mut Vec<String>,
|
||||
chapter: &Chapter,
|
||||
chapter_tree: &ChapterTree<'_>,
|
||||
) -> Result<()> {
|
||||
let chapter_path = chapter
|
||||
.path
|
||||
.as_ref()
|
||||
.expect("Checked that path exists above");
|
||||
let filepath = Path::new(&chapter_path).with_extension("html");
|
||||
let filepath = filepath
|
||||
.to_str()
|
||||
.with_context(|| "Could not convert HTML path to str")?;
|
||||
let anchor_base = utils::fs::normalize_path(filepath);
|
||||
|
||||
let mut p = utils::new_cmark_parser(&chapter.content, false).peekable();
|
||||
let anchor_base = chapter_tree.html_path.to_url_path();
|
||||
|
||||
let mut in_heading = false;
|
||||
let max_section_depth = u32::from(search_config.heading_split_level);
|
||||
let max_section_depth = search_config.heading_split_level;
|
||||
let mut section_id = None;
|
||||
let mut heading = String::new();
|
||||
let mut body = String::new();
|
||||
let mut breadcrumbs = chapter.parent_names.clone();
|
||||
let mut footnote_numbers = HashMap::new();
|
||||
let mut breadcrumbs = chapter_tree.chapter.parent_names.clone();
|
||||
|
||||
breadcrumbs.push(chapter.name.clone());
|
||||
breadcrumbs.push(chapter_tree.chapter.name.clone());
|
||||
|
||||
let mut id_counter = HashMap::new();
|
||||
while let Some(event) = p.next() {
|
||||
match event {
|
||||
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
|
||||
add_doc(
|
||||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
&heading,
|
||||
&mut id_counter,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
heading.clear();
|
||||
body.clear();
|
||||
breadcrumbs.pop();
|
||||
}
|
||||
let mut traverse = chapter_tree.tree.root().traverse();
|
||||
|
||||
section_id = id;
|
||||
in_heading = true;
|
||||
}
|
||||
Event::End(TagEnd::Heading(level)) if level as u32 <= max_section_depth => {
|
||||
in_heading = false;
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||
let number = footnote_numbers.len() + 1;
|
||||
footnote_numbers.entry(name).or_insert(number);
|
||||
}
|
||||
Event::Html(html) => {
|
||||
let mut html_block = html.into_string();
|
||||
|
||||
// As of pulldown_cmark 0.6, html events are no longer contained
|
||||
// in an HtmlBlock tag. We must collect consecutive Html events
|
||||
// into a block ourselves.
|
||||
while let Some(Event::Html(html)) = p.peek() {
|
||||
html_block.push_str(html);
|
||||
p.next();
|
||||
while let Some(edge) = traverse.next() {
|
||||
match edge {
|
||||
Edge::Open(node) => match node.value() {
|
||||
Node::Element(el) => {
|
||||
if let Some(level) = el.heading_level()
|
||||
&& level <= max_section_depth
|
||||
&& let Some(heading_id) = el.attr("id")
|
||||
{
|
||||
if !heading.is_empty() {
|
||||
// Section finished, the next heading is following now
|
||||
// Write the data to the index, and clear it for the next section
|
||||
add_doc(
|
||||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
section_id.unwrap(),
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
heading.clear();
|
||||
body.clear();
|
||||
breadcrumbs.pop();
|
||||
}
|
||||
section_id = Some(heading_id);
|
||||
in_heading = true;
|
||||
} else if matches!(el.name(), "script" | "style") {
|
||||
// Skip this node.
|
||||
while let Some(edge) = traverse.next() {
|
||||
if let Edge::Close(close) = edge
|
||||
&& close == node
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Insert spaces where HTML output would usually separate text
|
||||
// to ensure words don't get merged together
|
||||
} else if in_heading {
|
||||
heading.push(' ');
|
||||
} else {
|
||||
body.push(' ');
|
||||
}
|
||||
}
|
||||
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
|
||||
if in_heading {
|
||||
heading.push(' ');
|
||||
} else {
|
||||
body.push(' ');
|
||||
Node::Text(text) => {
|
||||
if in_heading {
|
||||
heading.push_str(text);
|
||||
} else {
|
||||
body.push_str(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Text(text) | Event::Code(text) => {
|
||||
if in_heading {
|
||||
heading.push_str(&text);
|
||||
} else {
|
||||
body.push_str(&text);
|
||||
Node::Comment(_) => {}
|
||||
Node::Fragment => {}
|
||||
Node::RawData(_) => {}
|
||||
},
|
||||
Edge::Close(node) => match node.value() {
|
||||
Node::Element(el) => {
|
||||
if let Some(level) = el.heading_level()
|
||||
&& level <= max_section_depth
|
||||
{
|
||||
in_heading = false;
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::FootnoteReference(name) => {
|
||||
let len = footnote_numbers.len() + 1;
|
||||
let number = footnote_numbers.entry(name).or_insert(len);
|
||||
body.push_str(&format!(" [{number}] "));
|
||||
}
|
||||
Event::TaskListMarker(_checked) => {}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if !body.is_empty() || !heading.is_empty() {
|
||||
// Make sure the last section is added to the index
|
||||
let title = if heading.is_empty() {
|
||||
if let Some(chapter) = breadcrumbs.first() {
|
||||
chapter
|
||||
@@ -236,14 +196,11 @@ fn render_item(
|
||||
} 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,
|
||||
section_id.unwrap_or_default(),
|
||||
&[title, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
}
|
||||
@@ -313,40 +270,20 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
|
||||
Ok(json_contents)
|
||||
}
|
||||
|
||||
fn clean_html(html: &str) -> String {
|
||||
static AMMONIA: LazyLock<ammonia::Builder<'static>> = LazyLock::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()
|
||||
}
|
||||
|
||||
fn settings_path(ch: &Chapter) -> Option<&Path> {
|
||||
ch.source_path.as_deref().or_else(|| ch.path.as_deref())
|
||||
fn settings_path(ch: &Chapter) -> &Path {
|
||||
ch.source_path
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| ch.path.as_deref().unwrap())
|
||||
}
|
||||
|
||||
fn validate_chapter_config(
|
||||
chapter_configs: &[(PathBuf, SearchChapterSettings)],
|
||||
book: &Book,
|
||||
chapter_trees: &[ChapterTree<'_>],
|
||||
) -> Result<()> {
|
||||
for (path, _) in chapter_configs {
|
||||
let found = book
|
||||
let found = chapter_trees
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
BookItem::Chapter(ch) if !ch.is_draft_chapter() => settings_path(ch),
|
||||
_ => None,
|
||||
})
|
||||
.any(|source_path| source_path.starts_with(path));
|
||||
.any(|ct| settings_path(ct.chapter).starts_with(path));
|
||||
if !found {
|
||||
bail!(
|
||||
"[output.html.search.chapter] key `{}` does not match any chapter paths",
|
||||
@@ -383,6 +320,12 @@ fn get_chapter_settings(
|
||||
result
|
||||
}
|
||||
|
||||
/// Replaces multiple consecutive whitespace characters with a single space character.
|
||||
fn collapse_whitespace(text: &str) -> Cow<'_, str> {
|
||||
static_regex!(WS, r"\s\s+");
|
||||
WS.replace_all(text, " ")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chapter_settings_priority() {
|
||||
let cfg = r#"
|
||||
@@ -393,7 +336,7 @@ fn chapter_settings_priority() {
|
||||
"cli/inner" = { enable = true }
|
||||
"foo" = {} # Just to make sure empty table is allowed.
|
||||
"#;
|
||||
let cfg: crate::Config = toml::from_str(cfg).unwrap();
|
||||
let cfg: mdbook_core::config::Config = toml::from_str(cfg).unwrap();
|
||||
let html = cfg.html_config().unwrap();
|
||||
let chapter_configs = sort_search_config(&html.search.unwrap().chapter);
|
||||
for (path, enable) in [
|
||||
@@ -403,9 +346,11 @@ fn chapter_settings_priority() {
|
||||
("cli/inner/index.md", Some(true)),
|
||||
("cli/inner/foo.md", Some(false)),
|
||||
] {
|
||||
let mut settings = SearchChapterSettings::default();
|
||||
settings.enable = enable;
|
||||
assert_eq!(
|
||||
get_chapter_settings(&chapter_configs, Path::new(path)),
|
||||
SearchChapterSettings { enable }
|
||||
settings
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
//! Support for writing static files.
|
||||
|
||||
use log::{debug, warn};
|
||||
|
||||
use crate::config::HtmlConfig;
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers::resources::ResourceHelper;
|
||||
use crate::theme::{self, playground_editor, Theme};
|
||||
use crate::utils;
|
||||
|
||||
use super::helpers::resources::ResourceHelper;
|
||||
use crate::theme::{self, Theme, playground_editor};
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook_core::config::HtmlConfig;
|
||||
use mdbook_core::static_regex;
|
||||
use mdbook_core::utils::fs;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
use tracing::debug;
|
||||
|
||||
/// Map static files to their final names and contents.
|
||||
///
|
||||
@@ -22,7 +19,7 @@ use std::sync::LazyLock;
|
||||
/// and interprets the `{{ resource }}` directives to allow assets to name each other.
|
||||
///
|
||||
/// [fingerprinting]: https://guides.rubyonrails.org/asset_pipeline.html#fingerprinting-versioning-with-digest-based-urls
|
||||
pub struct StaticFiles {
|
||||
pub(super) struct StaticFiles {
|
||||
static_files: Vec<StaticFile>,
|
||||
hash_map: HashMap<String, String>,
|
||||
}
|
||||
@@ -39,7 +36,7 @@ enum StaticFile {
|
||||
}
|
||||
|
||||
impl StaticFiles {
|
||||
pub fn new(theme: &Theme, html_config: &HtmlConfig, root: &Path) -> Result<StaticFiles> {
|
||||
pub(super) fn new(theme: &Theme, html_config: &HtmlConfig, root: &Path) -> Result<StaticFiles> {
|
||||
let static_files = Vec::new();
|
||||
let mut this = StaticFiles {
|
||||
hash_map: HashMap::new(),
|
||||
@@ -64,29 +61,7 @@ impl StaticFiles {
|
||||
this.add_builtin("ayu-highlight.css", &theme.ayu_highlight_css);
|
||||
this.add_builtin("highlight.js", &theme.highlight_js);
|
||||
this.add_builtin("clipboard.min.js", &theme.clipboard_js);
|
||||
this.add_builtin("FontAwesome/css/font-awesome.css", theme::FONT_AWESOME);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
);
|
||||
this.add_builtin("FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF);
|
||||
if html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
if theme.fonts_css.is_none() {
|
||||
this.add_builtin("fonts/fonts.css", theme::fonts::CSS);
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
this.add_builtin(file_name, contents);
|
||||
@@ -103,13 +78,6 @@ impl StaticFiles {
|
||||
this.add_builtin("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."
|
||||
);
|
||||
}
|
||||
|
||||
let playground_config = &html_config.playground;
|
||||
|
||||
@@ -158,7 +126,7 @@ impl StaticFiles {
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
pub fn add_builtin(&mut self, filename: &str, data: &[u8]) {
|
||||
pub(super) fn add_builtin(&mut self, filename: &str, data: &[u8]) {
|
||||
self.static_files.push(StaticFile::Builtin {
|
||||
filename: filename.to_owned(),
|
||||
data: data.to_owned(),
|
||||
@@ -167,25 +135,19 @@ impl StaticFiles {
|
||||
|
||||
/// Updates this [`StaticFiles`] to hash the contents for determining the
|
||||
/// filename for each resource.
|
||||
pub fn hash_files(&mut self) -> Result<()> {
|
||||
pub(super) fn hash_files(&mut self) -> Result<()> {
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::Read;
|
||||
for static_file in &mut self.static_files {
|
||||
match static_file {
|
||||
StaticFile::Builtin {
|
||||
&mut StaticFile::Builtin {
|
||||
ref mut filename,
|
||||
ref data,
|
||||
} => {
|
||||
let mut parts = filename.splitn(2, '.');
|
||||
let parts = parts.next().and_then(|p| Some((p, parts.next()?)));
|
||||
if let Some((name, suffix)) = parts {
|
||||
// FontAwesome already does its own cache busting with the ?v=4.7.0 thing,
|
||||
// and I don't want to have to patch its CSS file to use `{{ resource }}`
|
||||
if name != ""
|
||||
&& suffix != ""
|
||||
&& suffix != "txt"
|
||||
&& !name.starts_with("FontAwesome/fonts/")
|
||||
{
|
||||
if name != "" && suffix != "" && suffix != "txt" {
|
||||
let hex = hex::encode(&Sha256::digest(data)[..4]);
|
||||
let new_filename = format!("{}-{}.{}", name, hex, suffix);
|
||||
self.hash_map.insert(filename.clone(), new_filename.clone());
|
||||
@@ -193,7 +155,7 @@ impl StaticFiles {
|
||||
}
|
||||
}
|
||||
}
|
||||
StaticFile::Additional {
|
||||
&mut StaticFile::Additional {
|
||||
ref mut filename,
|
||||
ref input_location,
|
||||
} => {
|
||||
@@ -202,8 +164,10 @@ impl StaticFiles {
|
||||
if let Some((name, suffix)) = parts {
|
||||
if name != "" && suffix != "" {
|
||||
let mut digest = Sha256::new();
|
||||
let mut input_file = File::open(input_location)
|
||||
.with_context(|| "open static file for hashing")?;
|
||||
let mut input_file =
|
||||
std::fs::File::open(input_location).with_context(|| {
|
||||
format!("failed to open `{filename}` for hashing")
|
||||
})?;
|
||||
let mut buf = vec![0; 1024];
|
||||
loop {
|
||||
let amt = input_file
|
||||
@@ -226,13 +190,11 @@ impl StaticFiles {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_files(self, destination: &Path) -> Result<ResourceHelper> {
|
||||
use crate::utils::fs::write_file;
|
||||
use regex::bytes::{Captures, Regex};
|
||||
pub(super) fn write_files(self, destination: &Path) -> Result<ResourceHelper> {
|
||||
use regex::bytes::Captures;
|
||||
// The `{{ resource "name" }}` directive in static resources look like
|
||||
// handlebars syntax, even if they technically aren't.
|
||||
static RESOURCE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r#"\{\{ resource "([^"]+)" \}\}"#).unwrap());
|
||||
static_regex!(RESOURCE, bytes, r#"\{\{ resource "([^"]+)" \}\}"#);
|
||||
fn replace_all<'a>(
|
||||
hash_map: &HashMap<String, String>,
|
||||
data: &'a [u8],
|
||||
@@ -245,7 +207,7 @@ impl StaticFiles {
|
||||
.as_bytes();
|
||||
let name = std::str::from_utf8(name).expect("resource name with invalid utf8");
|
||||
let resource_filename = hash_map.get(name).map(|s| &s[..]).unwrap_or(name);
|
||||
let path_to_root = utils::fs::path_to_root(filename);
|
||||
let path_to_root = fs::path_to_root(filename);
|
||||
format!("{}{}", path_to_root, resource_filename)
|
||||
.as_bytes()
|
||||
.to_owned()
|
||||
@@ -260,11 +222,12 @@ impl StaticFiles {
|
||||
} else {
|
||||
Cow::Borrowed(&data[..])
|
||||
};
|
||||
write_file(destination, filename, &data)?;
|
||||
let path = destination.join(filename);
|
||||
fs::write(path, &data)?;
|
||||
}
|
||||
StaticFile::Additional {
|
||||
ref input_location,
|
||||
ref filename,
|
||||
input_location,
|
||||
filename,
|
||||
} => {
|
||||
let output_location = destination.join(filename);
|
||||
debug!(
|
||||
@@ -277,11 +240,12 @@ impl StaticFiles {
|
||||
.with_context(|| format!("Unable to create {}", parent.display()))?;
|
||||
}
|
||||
if filename.ends_with(".css") || filename.ends_with(".js") {
|
||||
let data = fs::read(input_location)?;
|
||||
let data = replace_all(&self.hash_map, &data, filename);
|
||||
write_file(destination, filename, &data)?;
|
||||
let data = fs::read_to_string(input_location)?;
|
||||
let data = replace_all(&self.hash_map, data.as_bytes(), filename);
|
||||
let path = destination.join(filename);
|
||||
fs::write(path, &data)?;
|
||||
} else {
|
||||
fs::copy(input_location, &output_location).with_context(|| {
|
||||
std::fs::copy(input_location, &output_location).with_context(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
input_location.display(),
|
||||
@@ -300,9 +264,9 @@ impl StaticFiles {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::HtmlConfig;
|
||||
use crate::theme::Theme;
|
||||
use crate::utils::fs::write_file;
|
||||
use mdbook_core::config::HtmlConfig;
|
||||
use mdbook_core::utils::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
@@ -333,9 +297,8 @@ mod tests {
|
||||
let reference_js = Path::new("static-files-test-case-reference.js");
|
||||
let mut html_config = HtmlConfig::default();
|
||||
html_config.additional_js.push(reference_js.to_owned());
|
||||
write_file(
|
||||
temp_dir.path(),
|
||||
reference_js,
|
||||
fs::write(
|
||||
temp_dir.path().join(reference_js),
|
||||
br#"{{ resource "book.js" }}"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -343,7 +306,7 @@ mod tests {
|
||||
static_files.hash_files().unwrap();
|
||||
static_files.write_files(temp_dir.path()).unwrap();
|
||||
// custom JS winds up referencing book.js
|
||||
let reference_js_content = std::fs::read_to_string(
|
||||
let reference_js_content = fs::read_to_string(
|
||||
temp_dir
|
||||
.path()
|
||||
.join("static-files-test-case-reference-635c9cdc.js"),
|
||||
@@ -351,8 +314,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!("book-e3b0c442.js", reference_js_content);
|
||||
// book.js winds up empty
|
||||
let book_js_content =
|
||||
std::fs::read_to_string(temp_dir.path().join("book-e3b0c442.js")).unwrap();
|
||||
let book_js_content = fs::read_to_string(temp_dir.path().join("book-e3b0c442.js")).unwrap();
|
||||
assert_eq!("", book_js_content);
|
||||
}
|
||||
}
|
||||
8
crates/mdbook-html/src/lib.rs
Normal file
8
crates/mdbook-html/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! mdBook HTML renderer.
|
||||
|
||||
mod html;
|
||||
mod html_handlebars;
|
||||
pub mod theme;
|
||||
pub(crate) mod utils;
|
||||
|
||||
pub use html_handlebars::HtmlHandlebars;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user