mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 17:21:52 -05:00
Compare commits
322 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6dd0a4a13 | ||
|
|
432b4296ab | ||
|
|
6b1dc01a3f | ||
|
|
9d8e99f8d7 | ||
|
|
e152e197c1 | ||
|
|
7e68d01e7d | ||
|
|
bd97611eb0 | ||
|
|
8e579072b8 | ||
|
|
a918910a52 | ||
|
|
1eeb0d23e6 | ||
|
|
c842b5d06e | ||
|
|
0ac89dd826 | ||
|
|
7ec083e426 | ||
|
|
15c93b56ed | ||
|
|
ec6f26e652 | ||
|
|
cdce9a7666 | ||
|
|
63432355e6 | ||
|
|
4bf2b472bb | ||
|
|
1476ec72c3 | ||
|
|
d1c09791ab | ||
|
|
16b99be17f | ||
|
|
bcd4552bdf | ||
|
|
f942f3835e | ||
|
|
e96c608c11 | ||
|
|
e6315bf2b1 | ||
|
|
6d63343b46 | ||
|
|
e1e4518499 | ||
|
|
34d68403da | ||
|
|
e395341210 | ||
|
|
f4c54178c8 | ||
|
|
63e1dac122 | ||
|
|
e8dff6f97e | ||
|
|
534666f551 | ||
|
|
f3ee794283 | ||
|
|
94f9a9c5e0 | ||
|
|
dc6b0a6e58 | ||
|
|
2fa13cf4e0 | ||
|
|
d64a863223 | ||
|
|
1fb91d67f6 | ||
|
|
1b046e5a90 | ||
|
|
e9f5e3d7c0 | ||
|
|
c6a5d05c64 | ||
|
|
f93b2675ff | ||
|
|
365918bf89 | ||
|
|
76be253f76 | ||
|
|
0210e69abc | ||
|
|
cdbf6d2806 | ||
|
|
902ded9f89 | ||
|
|
851932bd4b | ||
|
|
f38dc687e3 | ||
|
|
391ee3bae2 | ||
|
|
ad3096e871 | ||
|
|
179bd8dcd5 | ||
|
|
426e7bee17 | ||
|
|
564c80bf6d | ||
|
|
363c12e9c3 | ||
|
|
1ffa9fe830 | ||
|
|
91d13c390a | ||
|
|
ce8d8120f8 | ||
|
|
038b5fb232 | ||
|
|
fd768efba2 | ||
|
|
481346812c | ||
|
|
3d07798832 | ||
|
|
33da0c26c4 | ||
|
|
91e94024cd | ||
|
|
c9ad6dbf10 | ||
|
|
2b455fcd34 | ||
|
|
4573f4d882 | ||
|
|
63ae0d5c18 | ||
|
|
52e406bf95 | ||
|
|
3e871d1971 | ||
|
|
84a5ba9707 | ||
|
|
44d9f4e95b | ||
|
|
d24c0ca0d7 | ||
|
|
623fc606a4 | ||
|
|
a8aee21cd0 | ||
|
|
649a021647 | ||
|
|
785ee564c5 | ||
|
|
006f99ee99 | ||
|
|
2a4e5140c9 | ||
|
|
3c10b00096 | ||
|
|
c9ddb4dd98 | ||
|
|
9822c2a178 | ||
|
|
199efd0f2c | ||
|
|
17b197620b | ||
|
|
23abd20589 | ||
|
|
7e9be8dee3 | ||
|
|
09d22e926f | ||
|
|
1696f5680e | ||
|
|
a54ee0215e | ||
|
|
9b12c5130f | ||
|
|
084771bcd5 | ||
|
|
7215d60c67 | ||
|
|
0224190ec0 | ||
|
|
ae2fc9a9d1 | ||
|
|
d65d2b2a8e | ||
|
|
69972080f0 | ||
|
|
5f2453e446 | ||
|
|
20f71af4cb | ||
|
|
efc5ee4449 | ||
|
|
14d412b279 | ||
|
|
707319e004 | ||
|
|
bdd16e25fa | ||
|
|
9a1f983e65 | ||
|
|
c2c37705e7 | ||
|
|
5f227613aa | ||
|
|
0274ad6e87 | ||
|
|
dd27c4f8ba | ||
|
|
25b9acc321 | ||
|
|
10fae8596c | ||
|
|
909bd1c54e | ||
|
|
f324aebdec | ||
|
|
5a84d641cd | ||
|
|
0b577ebd76 | ||
|
|
2056c87e28 | ||
|
|
8bfa6462f8 | ||
|
|
a8660048ca | ||
|
|
cad8988f8d | ||
|
|
3fce1151dd | ||
|
|
d23bdaa527 | ||
|
|
2f10831a80 | ||
|
|
a38a30da1e | ||
|
|
82000d917f | ||
|
|
f482aeaca3 | ||
|
|
86638abea9 | ||
|
|
a2cf838baf | ||
|
|
5bc25e32eb | ||
|
|
15c6f3f318 | ||
|
|
cb2a63ea0a | ||
|
|
50dfa365c7 | ||
|
|
3e22a5cdad | ||
|
|
5034707a73 | ||
|
|
d815b0cc52 | ||
|
|
fca149a52c | ||
|
|
b4221680e4 | ||
|
|
ba448a9dd5 | ||
|
|
aa29ef04a2 | ||
|
|
20d42a53d3 | ||
|
|
8c8f0a4dbf | ||
|
|
6904653a82 | ||
|
|
74e01ea6e3 | ||
|
|
0732cb47b9 | ||
|
|
29338b5ade | ||
|
|
4019060ef4 | ||
|
|
3e1d750efa | ||
|
|
41bfbc69e6 | ||
|
|
6fdd7b4a17 | ||
|
|
c6d9f15cba | ||
|
|
0f397ebdb5 | ||
|
|
342b6ee7b5 | ||
|
|
9952ac15a5 | ||
|
|
7add0dbf10 | ||
|
|
03470a7531 | ||
|
|
dd778d50f9 | ||
|
|
ac3e4b6c1e | ||
|
|
3706ddc5cc | ||
|
|
adcea9b3b9 | ||
|
|
ba8107120c | ||
|
|
b9e433710d | ||
|
|
f10d23e893 | ||
|
|
12b4a9631a | ||
|
|
36fa0064de | ||
|
|
566a42c4f7 | ||
|
|
6e143ce2a1 | ||
|
|
46963ebf65 | ||
|
|
c948fe4d6a | ||
|
|
b0ef5a54cc | ||
|
|
fbc875dd9f | ||
|
|
e6b1413d22 | ||
|
|
1d3b99c0df | ||
|
|
8181445d99 | ||
|
|
14aeb0cb83 | ||
|
|
3e6b42cfba | ||
|
|
c57a8fcfc4 | ||
|
|
9d6fcc9afe | ||
|
|
ea8f0f6161 | ||
|
|
06e8f6f849 | ||
|
|
36e5525ea5 | ||
|
|
e5d5f5d02b | ||
|
|
98088c91dd | ||
|
|
a4f7d11e92 | ||
|
|
4c7e85ba82 | ||
|
|
9693c4af05 | ||
|
|
3052fe3827 | ||
|
|
c85c3eb292 | ||
|
|
0d734bbb03 | ||
|
|
b9b34f97d9 | ||
|
|
ee59e22603 | ||
|
|
22a6dca69b | ||
|
|
4f698f813c | ||
|
|
97f1948681 | ||
|
|
7acc7a03a8 | ||
|
|
0ed1cbe486 | ||
|
|
2c382a58d3 | ||
|
|
b7a27d2759 | ||
|
|
d67dbc74fd | ||
|
|
d9d27f38c3 | ||
|
|
4886c92fa4 | ||
|
|
195d97a514 | ||
|
|
e954e872f0 | ||
|
|
e74b4b0507 | ||
|
|
4946c78e8c | ||
|
|
54d8d37b77 | ||
|
|
20eea0b41e | ||
|
|
8835bdc47e | ||
|
|
7a1977a78c | ||
|
|
629c09df4d | ||
|
|
7247e5f9a1 | ||
|
|
a3c0ecdb45 | ||
|
|
b20b1757a9 | ||
|
|
1bc2ebd775 | ||
|
|
a56cffeb4e | ||
|
|
c908ac8cc5 | ||
|
|
b47d1cff33 | ||
|
|
780daa73ae | ||
|
|
9114905a93 | ||
|
|
a7aaef1e85 | ||
|
|
9823246ecd | ||
|
|
861940ba4b | ||
|
|
3ed302467e | ||
|
|
f54356da10 | ||
|
|
418d677584 | ||
|
|
a0eb8c0a0e | ||
|
|
67b4260021 | ||
|
|
7c6d47e8b6 | ||
|
|
43281c85c5 | ||
|
|
e73d3b7cfa | ||
|
|
1de8cf8ba6 | ||
|
|
85afbe466e | ||
|
|
63b312948a | ||
|
|
3a8faba645 | ||
|
|
6d6bee0dc9 | ||
|
|
4b266f1ebc | ||
|
|
fc7ef59dee | ||
|
|
5fa9f12427 | ||
|
|
07b25cdb64 | ||
|
|
105d836fbc | ||
|
|
74fcaf5273 | ||
|
|
74200f7395 | ||
|
|
a7ca2e169f | ||
|
|
1a5286b25c | ||
|
|
c493d3b5e3 | ||
|
|
a68091a84c | ||
|
|
32daca669a | ||
|
|
cf5a78c0e1 | ||
|
|
b0cf568ba4 | ||
|
|
bf544be282 | ||
|
|
4f0dba8fdb | ||
|
|
5390e44dec | ||
|
|
0fb814c6d6 | ||
|
|
e7e3317ff0 | ||
|
|
d68a596455 | ||
|
|
ace2abff34 | ||
|
|
0c6439faad | ||
|
|
e7418f21f9 | ||
|
|
19146c403e | ||
|
|
66ded2302f | ||
|
|
98abb22be1 | ||
|
|
ab304e7d38 | ||
|
|
fbc21592af | ||
|
|
e7b69114ed | ||
|
|
8a9ecd212d | ||
|
|
ec157cd1cd | ||
|
|
d3bcb359fa | ||
|
|
2a4e5583ab | ||
|
|
3978612611 | ||
|
|
4941acdb87 | ||
|
|
7e3d2f96ab | ||
|
|
ddba36b24c | ||
|
|
35cf96a064 | ||
|
|
5777a0edc4 | ||
|
|
53c3a92285 | ||
|
|
82db7f5b93 | ||
|
|
879449447f | ||
|
|
132ca0dca3 | ||
|
|
56c2b9ba3a | ||
|
|
542b6feed1 | ||
|
|
2af44a396f | ||
|
|
40d91fff29 | ||
|
|
59eab7cfc2 | ||
|
|
1b524ff356 | ||
|
|
9b873e9d97 | ||
|
|
b6d6cb2711 | ||
|
|
c8095160d0 | ||
|
|
ae6db3a87e | ||
|
|
18f57f5bd9 | ||
|
|
09a37284b0 | ||
|
|
dff5ac64e5 | ||
|
|
0ee565a5ff | ||
|
|
9e4854f349 | ||
|
|
74d48f5ad2 | ||
|
|
0b51a74c16 | ||
|
|
ce63cc31f4 | ||
|
|
d6720fc671 | ||
|
|
64cca1399b | ||
|
|
629c2ad2fd | ||
|
|
d325e821cd | ||
|
|
ac3a7faa54 | ||
|
|
35ed24cd18 | ||
|
|
81d42f1c6e | ||
|
|
618a2fa78b | ||
|
|
0bf6751eed | ||
|
|
f92eac4acd | ||
|
|
69ef52fd13 | ||
|
|
cc8ce35b4d | ||
|
|
2a13ca2fbf | ||
|
|
59e6afcaad | ||
|
|
4d9a455a27 | ||
|
|
74b2c79d46 | ||
|
|
ed407b091c | ||
|
|
6c8020a3b9 | ||
|
|
42f18d1e51 | ||
|
|
abf3e4ab50 | ||
|
|
d1078434af | ||
|
|
8f024dabc3 | ||
|
|
0c580c32c4 | ||
|
|
90960126e8 | ||
|
|
aa37f24fc1 | ||
|
|
3f4f287e6e | ||
|
|
55fe75c716 | ||
|
|
c6236ead67 | ||
|
|
68e3572278 |
95
.eslintrc.json
Normal file
95
.eslintrc.json
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -6,3 +6,5 @@
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.png binary
|
||||
*.eot binary
|
||||
*.woff2 binary
|
||||
|
||||
10
.github/workflows/deploy.yml
vendored
10
.github/workflows/deploy.yml
vendored
@@ -17,13 +17,15 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
name: Deploy ${{ matrix.target }}
|
||||
@@ -46,7 +48,7 @@ jobs:
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Build book
|
||||
run: cargo run -- build guide
|
||||
- name: Deploy to GitHub
|
||||
- name: Deploy the User Guide to GitHub Pages using the gh-pages branch
|
||||
env:
|
||||
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
|
||||
run: |
|
||||
|
||||
52
.github/workflows/main.yml
vendored
52
.github/workflows/main.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
rust: nightly
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: stable x86_64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-musl
|
||||
- name: stable x86_64 macos
|
||||
@@ -38,9 +38,9 @@ jobs:
|
||||
rust: stable
|
||||
target: x86_64-pc-windows-msvc
|
||||
- name: msrv
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
|
||||
rust: 1.74.0
|
||||
rust: 1.82.0
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: ${{ matrix.name }}
|
||||
steps:
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
run: cargo test --no-default-features --target ${{ matrix.target }}
|
||||
|
||||
aarch64-cross-builds:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
@@ -70,6 +70,46 @@ jobs:
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt --check
|
||||
|
||||
gui:
|
||||
name: GUI tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- name: Install npm
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install browser-ui-test
|
||||
run: npm install
|
||||
- name: Run eslint
|
||||
run: npm run lint
|
||||
- name: Build and run tests (+ GUI)
|
||||
run: cargo test --locked --target x86_64-unknown-linux-gnu --test gui
|
||||
|
||||
# Ensure there are no clippy warnings
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- run: rustup component add clippy
|
||||
- run: cargo clippy --workspace --all-targets --no-deps -- -D warnings
|
||||
|
||||
docs:
|
||||
name: Check API docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- name: Ensure intradoc links are valid
|
||||
run: cargo doc --workspace --document-private-items --no-deps
|
||||
env:
|
||||
RUSTDOCFLAGS: -D warnings
|
||||
|
||||
# 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
|
||||
@@ -81,6 +121,10 @@ jobs:
|
||||
needs:
|
||||
- test
|
||||
- rustfmt
|
||||
- aarch64-cross-builds
|
||||
- gui
|
||||
- clippy
|
||||
- docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -9,6 +9,7 @@ guide/book
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
test_book/book/
|
||||
tests/testsuite/*/*/book/
|
||||
|
||||
# Ignore Jetbrains specific files.
|
||||
.idea/
|
||||
@@ -16,3 +17,8 @@ test_book/book/
|
||||
# Ignore Vim temporary and swap files.
|
||||
*.sw?
|
||||
*~
|
||||
|
||||
# GUI tests
|
||||
node_modules
|
||||
package-lock.json
|
||||
package.json
|
||||
|
||||
204
CHANGELOG.md
204
CHANGELOG.md
@@ -1,8 +1,212 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.4.52
|
||||
[v0.4.51...v0.4.52](https://github.com/rust-lang/mdBook/compare/v0.4.51...v0.4.52)
|
||||
|
||||
**Note:** If you have a custom `index.hbs` theme file, it is recommended that you update it to the latest version to pick up the fixes in this release.
|
||||
|
||||
### Added
|
||||
- Added the ability to redirect `#` HTML fragments using the existing `output.html.redirect` table.
|
||||
[#2747](https://github.com/rust-lang/mdBook/pull/2747)
|
||||
- Added the `rel="edit"` attribute to the edit page button.
|
||||
[#2702](https://github.com/rust-lang/mdBook/pull/2702)
|
||||
|
||||
### Changed
|
||||
- The search index is now only loaded when the search input is opened instead of always being loaded.
|
||||
[#2553](https://github.com/rust-lang/mdBook/pull/2553)
|
||||
[#2735](https://github.com/rust-lang/mdBook/pull/2735)
|
||||
- The `mdbook serve` command has switched its underlying server library from warp to axum.
|
||||
[#2748](https://github.com/rust-lang/mdBook/pull/2748)
|
||||
- Updated dependencies.
|
||||
[#2752](https://github.com/rust-lang/mdBook/pull/2752)
|
||||
|
||||
### Fixed
|
||||
- The sidebar is now set to `display:none` when it is hidden in order to prevent the browser's search from thinking the sidebar's text is visible.
|
||||
[#2725](https://github.com/rust-lang/mdBook/pull/2725)
|
||||
- Fixed search index URL not updating correctly when `hash-files` is enabled.
|
||||
[#2742](https://github.com/rust-lang/mdBook/pull/2742)
|
||||
[#2746](https://github.com/rust-lang/mdBook/pull/2746)
|
||||
- Fixed several sidebar animation bugs, particularly when manually resizing.
|
||||
[#2750](https://github.com/rust-lang/mdBook/pull/2750)
|
||||
|
||||
## mdBook 0.4.51
|
||||
[v0.4.50...v0.4.51](https://github.com/rust-lang/mdBook/compare/v0.4.50...v0.4.51)
|
||||
|
||||
### Fixed
|
||||
- Fixed regression that broke the `S` search hotkey.
|
||||
[#2713](https://github.com/rust-lang/mdBook/pull/2713)
|
||||
|
||||
## mdBook 0.4.50
|
||||
[v0.4.49...v0.4.50](https://github.com/rust-lang/mdBook/compare/v0.4.49...v0.4.50)
|
||||
|
||||
### Added
|
||||
|
||||
- Added a keyboard shortcut help popup when pressing `?`.
|
||||
[#2608](https://github.com/rust-lang/mdBook/pull/2608)
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the look of the sidebar resize handle to match the new rustdoc format.
|
||||
[#2691](https://github.com/rust-lang/mdBook/pull/2691)
|
||||
- `/` can now be used to open the search bar.
|
||||
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
|
||||
- Pressing enter from the search bar will navigate to the first entry.
|
||||
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
|
||||
- Updated `opener` to drop some dependencies.
|
||||
[#2709](https://github.com/rust-lang/mdBook/pull/2709)
|
||||
- Updated dependencies, MSRV raised to 1.82.
|
||||
[#2711](https://github.com/rust-lang/mdBook/pull/2711)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed uncaught exception when pressing down when there are no search results.
|
||||
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
|
||||
- Fixed syntax highlighting of Rust code in the ACE editor.
|
||||
[#2710](https://github.com/rust-lang/mdBook/pull/2710)
|
||||
|
||||
## mdBook 0.4.49
|
||||
[v0.4.48...v0.4.49](https://github.com/rust-lang/mdBook/compare/v0.4.48...v0.4.49)
|
||||
|
||||
### Added
|
||||
|
||||
- Added a warning on unused fields in the root of `book.toml`.
|
||||
[#2622](https://github.com/rust-lang/mdBook/pull/2622)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated dependencies.
|
||||
[#2650](https://github.com/rust-lang/mdBook/pull/2650)
|
||||
[#2688](https://github.com/rust-lang/mdBook/pull/2688)
|
||||
- Updated minimum Rust version to 1.81.
|
||||
[#2688](https://github.com/rust-lang/mdBook/pull/2688)
|
||||
- The unused `book.multilingual` field is no longer serialized, or shown in `mdbook init`.
|
||||
[#2689](https://github.com/rust-lang/mdBook/pull/2689)
|
||||
- Speed up search index loading by using `JSON.parse` instead of parsing JavaScript.
|
||||
[#2633](https://github.com/rust-lang/mdBook/pull/2633)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Search highlighting will not try to highlight in SVG `<text>` elements because it breaks the element.
|
||||
[#2668](https://github.com/rust-lang/mdBook/pull/2668)
|
||||
- Fixed scrolling of the sidebar when a search highlight term is in the URL.
|
||||
[#2675](https://github.com/rust-lang/mdBook/pull/2675)
|
||||
- Fixed issues when multiple footnote definitions use the same ID. Now, only one definition is used, and a warning is displayed.
|
||||
[#2681](https://github.com/rust-lang/mdBook/pull/2681)
|
||||
- The sidebar is now restricted to 80% of the viewport width to make it possible to collapse it when the viewport is very narrow.
|
||||
[#2679](https://github.com/rust-lang/mdBook/pull/2679)
|
||||
|
||||
## mdBook 0.4.48
|
||||
[v0.4.47...v0.4.48](https://github.com/rust-lang/mdBook/compare/v0.4.47...v0.4.48)
|
||||
|
||||
### Added
|
||||
|
||||
- Footnotes now have back-reference links. These links bring the reader back to the original location. As part of this change, footnotes are now only rendered at the bottom of the page. This also includes some styling updates and fixes for footnote rendering.
|
||||
[#2626](https://github.com/rust-lang/mdBook/pull/2626)
|
||||
- Added an "Auto" theme selection option which will default to the system-preferred mode. This will also automatically switch when the system changes the preferred mode.
|
||||
[#2576](https://github.com/rust-lang/mdBook/pull/2576)
|
||||
|
||||
### Changed
|
||||
|
||||
- The `searchindex.json` file has been removed; only the `searchindex.js` file will be generated.
|
||||
[#2552](https://github.com/rust-lang/mdBook/pull/2552)
|
||||
- Updated Javascript code to use eslint.
|
||||
[#2554](https://github.com/rust-lang/mdBook/pull/2554)
|
||||
- An error is generated if there are duplicate files in `SUMMARY.md`.
|
||||
[#2613](https://github.com/rust-lang/mdBook/pull/2613)
|
||||
|
||||
## mdBook 0.4.47
|
||||
[v0.4.46...v0.4.47](https://github.com/rust-lang/mdBook/compare/v0.4.46...v0.4.47)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed search not showing up in sub-directories.
|
||||
[#2586](https://github.com/rust-lang/mdBook/pull/2586)
|
||||
|
||||
## mdBook 0.4.46
|
||||
[v0.4.45...v0.4.46](https://github.com/rust-lang/mdBook/compare/v0.4.45...v0.4.46)
|
||||
|
||||
### Changed
|
||||
|
||||
- The `output.html.hash-files` config option has been added to add hashes to static filenames to bust any caches when a book is updated. `{{resource}}` template tags have been added so that links can be properly generated to those files.
|
||||
[#1368](https://github.com/rust-lang/mdBook/pull/1368)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Playground links for Rust 2024 now set the edition correctly.
|
||||
[#2557](https://github.com/rust-lang/mdBook/pull/2557)
|
||||
|
||||
## mdBook 0.4.45
|
||||
[v0.4.44...v0.4.45](https://github.com/rust-lang/mdBook/compare/v0.4.44...v0.4.45)
|
||||
|
||||
### Changed
|
||||
|
||||
- Added context to error message when rustdoc is not found.
|
||||
[#2545](https://github.com/rust-lang/mdBook/pull/2545)
|
||||
- Slightly changed the styling rules around margins of footnotes.
|
||||
[#2524](https://github.com/rust-lang/mdBook/pull/2524)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where it would panic if a source_path is not set.
|
||||
[#2550](https://github.com/rust-lang/mdBook/pull/2550)
|
||||
|
||||
## mdBook 0.4.44
|
||||
[v0.4.43...v0.4.44](https://github.com/rust-lang/mdBook/compare/v0.4.43...v0.4.44)
|
||||
|
||||
### Added
|
||||
|
||||
- Added pre-built aarch64-apple-darwin binaries to the releases.
|
||||
[#2500](https://github.com/rust-lang/mdBook/pull/2500)
|
||||
- `mdbook clean` now shows a summary of what it did.
|
||||
[#2458](https://github.com/rust-lang/mdBook/pull/2458)
|
||||
- Added the `output.html.search.chapter` config setting to disable search indexing of individual chapters.
|
||||
[#2533](https://github.com/rust-lang/mdBook/pull/2533)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed auto-scrolling the side-bar when loading a page with a `#` fragment URL.
|
||||
[#2517](https://github.com/rust-lang/mdBook/pull/2517)
|
||||
- Fixed display of sidebar when javascript is disabled.
|
||||
[#2529](https://github.com/rust-lang/mdBook/pull/2529)
|
||||
- Fixed the sidebar visibility getting out of sync with the button.
|
||||
[#2532](https://github.com/rust-lang/mdBook/pull/2532)
|
||||
|
||||
### Changed
|
||||
|
||||
- ❗ Rust code block hidden lines now follow the same logic as rustdoc. This requires a space after the `#` symbol.
|
||||
[#2530](https://github.com/rust-lang/mdBook/pull/2530)
|
||||
- ❗ Updated the Linux pre-built binaries which requires a newer version of glibc (2.34).
|
||||
[#2523](https://github.com/rust-lang/mdBook/pull/2523)
|
||||
- Updated dependencies
|
||||
[#2538](https://github.com/rust-lang/mdBook/pull/2538)
|
||||
[#2539](https://github.com/rust-lang/mdBook/pull/2539)
|
||||
|
||||
## mdBook 0.4.43
|
||||
[v0.4.42...v0.4.43](https://github.com/rust-lang/mdBook/compare/v0.4.42...v0.4.43)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed setting the title in `mdbook init` when no git user is configured.
|
||||
[#2486](https://github.com/rust-lang/mdBook/pull/2486)
|
||||
|
||||
### Changed
|
||||
|
||||
- The Rust 2024 edition no longer needs `-Zunstable-options`.
|
||||
[#2495](https://github.com/rust-lang/mdBook/pull/2495)
|
||||
|
||||
## mdBook 0.4.42
|
||||
[v0.4.41...v0.4.42](https://github.com/rust-lang/mdBook/compare/v0.4.41...v0.4.42)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed chapter list folding.
|
||||
[#2473](https://github.com/rust-lang/mdBook/pull/2473)
|
||||
|
||||
## mdBook 0.4.41
|
||||
[v0.4.40...v0.4.41](https://github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)
|
||||
|
||||
**Note:** If you have a custom `index.hbs` theme file, you will need to update it to the latest version.
|
||||
|
||||
### Added
|
||||
|
||||
- Added preliminary support for Rust 2024 edition.
|
||||
|
||||
@@ -7,7 +7,7 @@ If you have come here to learn how to contribute to mdBook, we have some tips fo
|
||||
First of all, don't hesitate to ask questions!
|
||||
Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple.
|
||||
|
||||
### Issue assignment
|
||||
## Issue assignment
|
||||
|
||||
**:warning: Important :warning:**
|
||||
|
||||
@@ -16,7 +16,7 @@ The current PR backlog is beyond what we can process at this time.
|
||||
Only issues that have an [`E-Help-wanted`](https://github.com/rust-lang/mdBook/labels/E-Help-wanted) or [`Feature accepted`](https://github.com/rust-lang/mdBook/labels/Feature%20accepted) label will likely receive reviews.
|
||||
If there isn't already an open issue for what you want to work on, please open one first to see if it is something we would be available to review.
|
||||
|
||||
### Issues to work on
|
||||
## Issues to work on
|
||||
|
||||
If you are starting out, you might be interested in the
|
||||
[E-Easy issues](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||
@@ -41,7 +41,7 @@ Issues on the issue tracker are categorized with the following labels:
|
||||
- **S**-prefixed labels show the status of the issue
|
||||
- **C**-prefixed labels show the category of issue
|
||||
|
||||
### Building mdBook
|
||||
## Building mdBook
|
||||
|
||||
mdBook builds on stable Rust, if you want to build mdBook from source, here are the steps to follow:
|
||||
|
||||
@@ -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,8 +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.
|
||||
@@ -99,7 +98,7 @@ Like formatting your code with `rustfmt`, running clippy regularly and before yo
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
### Change requirements
|
||||
## Change requirements
|
||||
|
||||
Please consider the following when making a change:
|
||||
|
||||
@@ -124,7 +123,7 @@ 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.
|
||||
|
||||
### Making a pull-request
|
||||
## Making a pull-request
|
||||
|
||||
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||
One of the core maintainers will then approve the changes or request some changes before it gets merged.
|
||||
@@ -138,8 +137,45 @@ We generally strive to keep mdBook compatible with a relatively recent browser o
|
||||
That is, supporting Chrome, Safari, Firefox, Edge on Windows, macOS, Linux, iOS, and Android.
|
||||
If possible, do your best to avoid breaking older browser releases.
|
||||
|
||||
Any change to the HTML or styling is encouraged to manually check on as many browsers and platforms that you can.
|
||||
Unfortunately at this time we don't have any automated UI or browser testing, so your assistance in testing is appreciated.
|
||||
GUI tests are checked with the GUI testsuite. To run it, you need to install `npm` first. Then run:
|
||||
|
||||
```
|
||||
cargo test --test gui
|
||||
```
|
||||
|
||||
If you want to only run some tests, you can filter them by passing (part of) their name:
|
||||
|
||||
```
|
||||
cargo test --test gui -- search
|
||||
```
|
||||
|
||||
The first time, it'll fail and ask you to install the `browser-ui-test` package. Install it with the provided
|
||||
command then re-run the tests.
|
||||
|
||||
If you want to disable the headless mode, use the `--disable-headless-test` option:
|
||||
|
||||
```
|
||||
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).
|
||||
|
||||
### Checking changes in `.js` files
|
||||
|
||||
The `.js` files source code is checked using [`eslint`](https://eslint.org/). This is a linter (just like `clippy` in Rust)
|
||||
for the Javascript language. You can install it with `npm` by running the following command:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Then you can run it using:
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## Updating highlight.js
|
||||
|
||||
|
||||
1701
Cargo.lock
generated
1701
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
39
Cargo.toml
39
Cargo.toml
@@ -1,9 +1,15 @@
|
||||
[workspace]
|
||||
members = [".", "examples/remove-emphasis/mdbook-remove-emphasis"]
|
||||
|
||||
[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
|
||||
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.41"
|
||||
version = "0.4.52"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
@@ -17,56 +23,57 @@ license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
description = "Creates a book from markdown files"
|
||||
rust-version = "1.74"
|
||||
rust-version = "1.82" # Keep in sync with installation.md and .github/workflows/main.yml
|
||||
|
||||
[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"
|
||||
once_cell = "1.17.1"
|
||||
env_logger = "0.11.1"
|
||||
handlebars = "6.0"
|
||||
hex = "0.4.3"
|
||||
log = "0.4.17"
|
||||
memchr = "2.5.0"
|
||||
opener = "0.7.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"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "6.1.1", optional = true }
|
||||
notify-debouncer-mini = { version = "0.4.1", optional = true }
|
||||
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 }
|
||||
|
||||
# Serve feature
|
||||
futures-util = { version = "0.3.28", optional = true }
|
||||
tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.6", default-features = false, features = ["websocket"], optional = true }
|
||||
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 }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.11"
|
||||
predicates = "3.0.3"
|
||||
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"
|
||||
|
||||
[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:warp"]
|
||||
serve = ["dep:futures-util", "dep:tokio", "dep:axum", "dep:tower-http"]
|
||||
search = ["dep:elasticlunr-rs", "dep:ammonia"]
|
||||
|
||||
[[bin]]
|
||||
@@ -82,3 +89,13 @@ name = "remove-emphasis"
|
||||
path = "examples/remove-emphasis/test.rs"
|
||||
crate-type = ["lib"]
|
||||
test = true
|
||||
|
||||
[[test]]
|
||||
harness = false
|
||||
test = false
|
||||
name = "gui"
|
||||
path = "tests/gui/runner.rs"
|
||||
crate-type = ["bin"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# mdBook
|
||||
|
||||
[](https://github.com/rust-lang/mdBook/actions?workflow=CI)
|
||||
[](https://github.com/rust-lang/mdBook/actions/workflows/main.yml)
|
||||
[](https://crates.io/crates/mdbook)
|
||||
[](LICENSE)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ fn main() {
|
||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||
handle_supports(&preprocessor, sub_args);
|
||||
} else if let Err(e) = handle_preprocessing(&preprocessor) {
|
||||
eprintln!("{e}");
|
||||
eprintln!("{e:?}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ fn main() {
|
||||
}
|
||||
|
||||
if let Err(e) = handle_preprocessing() {
|
||||
eprintln!("{}", e);
|
||||
eprintln!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use mdbook::MDBook;
|
||||
|
||||
#[test]
|
||||
fn remove_emphasis_works() {
|
||||
// Tests that the remove-emphasis example works as expected.
|
||||
|
||||
// Workaround for https://github.com/rust-lang/mdBook/issues/1424
|
||||
std::env::set_current_dir("examples/remove-emphasis").unwrap();
|
||||
let book = MDBook::load(".").unwrap();
|
||||
let book = mdbook::MDBook::load(".").unwrap();
|
||||
book.build().unwrap();
|
||||
let ch1 = std::fs::read_to_string("book/chapter_1.html").unwrap();
|
||||
assert!(ch1.contains("This has light emphasis and bold emphasis."));
|
||||
|
||||
@@ -13,6 +13,7 @@ mathjax-support = true
|
||||
site-url = "/mdBook/"
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
|
||||
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
hash-files = true
|
||||
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
|
||||
@@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex
|
||||
|
||||
```sh
|
||||
mkdir bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.41/mdbook-v0.4.41-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.52/mdbook-v0.4.52-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
bin/mdbook build
|
||||
```
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ edition = "2015" # the default edition for code blocks
|
||||
|
||||
- **edition**: Rust edition to use by default for the code snippets. Default
|
||||
is `"2015"`. Individual code blocks can be controlled with the `edition2015`,
|
||||
`edition2018` or `edition2021` annotations, such as:
|
||||
`edition2018`, `edition2021` or `edition2024` annotations, such as:
|
||||
|
||||
~~~text
|
||||
```rust,edition2015
|
||||
|
||||
@@ -168,6 +168,12 @@ The following configuration options are available:
|
||||
This string will be written to a file named CNAME in the root of your site, as
|
||||
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages
|
||||
site*][custom domain]).
|
||||
- **hash-files:** Include a cryptographic "fingerprint" of the files' contents in static asset filenames,
|
||||
so that if the contents of the file are changed, the name of the file will also change.
|
||||
For example, `css/chrome.css` may become `css/chrome-9b8f428e.css`.
|
||||
Chapter HTML files are not renamed.
|
||||
Static CSS and JS files can reference each other using `{{ resource "filename" }}` directives.
|
||||
Defaults to `false` (in a future release, this may change to `true`).
|
||||
|
||||
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||
|
||||
@@ -281,6 +287,20 @@ copy-js = true # include Javascript code for search
|
||||
- **copy-js:** Copy JavaScript files for the search implementation to the output
|
||||
directory. Defaults to `true`.
|
||||
|
||||
#### `[output.html.search.chapter]`
|
||||
|
||||
The [`output.html.search.chapter`] table provides the ability to modify search settings per chapter or directory. Each key is the path to the chapter source file or directory, and the value is a table of settings to apply to that path. This will merge recursively, with more specific paths taking precedence.
|
||||
|
||||
```toml
|
||||
[output.html.search.chapter]
|
||||
# Disables search indexing for all chapters in the `appendix` directory.
|
||||
"appendix" = { enable = false }
|
||||
# Enables search indexing for just this one appendix chapter.
|
||||
"appendix/glossary.md" = { enable = true }
|
||||
```
|
||||
|
||||
- **enable:** Enables or disables search indexing for the given chapters. Defaults to `true`. This does not override the overall `output.html.search.enable` setting; that must be `true` for any search functionality to be enabled. Be cautious when disabling indexing for chapters because that can potentially lead to user confusion when they search for terms and expect them to be found. This should only be used in exceptional circumstances where keeping the chapter in the index will cause issues with the quality of the search results.
|
||||
|
||||
### `[output.html.redirect]`
|
||||
|
||||
The `[output.html.redirect]` table provides a way to add redirects.
|
||||
@@ -290,13 +310,21 @@ This is useful when you move, rename, or remove a page to ensure that links to t
|
||||
[output.html.redirect]
|
||||
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
|
||||
"/other-installation-methods.html" = "../infra/other-installation-methods.html"
|
||||
|
||||
# Fragment redirects also work.
|
||||
"/some-existing-page.html#old-fragment" = "some-existing-page.html#new-fragment"
|
||||
|
||||
# Fragment redirects also work for deleted pages.
|
||||
"/old-page.html" = "new-page.html"
|
||||
"/old-page.html#old-fragment" = "new-page.html#new-fragment"
|
||||
```
|
||||
|
||||
The table contains key-value pairs where the key is where the redirect file needs to be created, as an absolute path from the build directory, (e.g. `/appendices/bibliography.html`).
|
||||
The value can be any valid URI the browser should navigate to (e.g. `https://rust-lang.org/`, `/overview.html`, or `../bibliography.html`).
|
||||
|
||||
This will generate an HTML page which will automatically redirect to the given location.
|
||||
Note that the source location does not support `#` anchor redirects.
|
||||
|
||||
When fragment redirects are specified, the page must use JavaScript to redirect to the correct location. This is useful if you rename or move a section header. Fragment redirects work with existing pages and deleted pages.
|
||||
|
||||
## Markdown Renderer
|
||||
|
||||
|
||||
@@ -75,15 +75,22 @@ Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
Read about [mdBook](mdbook.md).
|
||||
|
||||
And now [an mdBook link] that is not inline, unlike the above.
|
||||
|
||||
A bare url: <https://www.rust-lang.org>.
|
||||
|
||||
[an mdBook link]: https://github.com/rust-lang/mdBook
|
||||
```
|
||||
|
||||
Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
Read about [mdBook](mdbook.md).
|
||||
|
||||
And now [an mdBook link] that is not inline, unlike the above.
|
||||
|
||||
A bare url: <https://www.rust-lang.org>.
|
||||
|
||||
[an mdBook link]: https://github.com/rust-lang/mdBook
|
||||
----
|
||||
|
||||
Relative links that end with `.md` will be converted to the `.html` extension.
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them with a specific prefix.
|
||||
|
||||
For the Rust language, you can use the `#` character as a prefix which will hide lines [like you would with Rustdoc][rustdoc-hide].
|
||||
For the Rust language, you can prefix lines with `# ` (`#` followed by a space) to hide them [like you would with Rustdoc][rustdoc-hide].
|
||||
This prefix can be escaped with `##` to prevent the hiding of a line that should begin with the literal string `# ` (see [Rustdoc's docs][rustdoc-hide] for more details)
|
||||
|
||||
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/write-documentation/documentation-tests.html#hiding-portions-of-the-example
|
||||
|
||||
@@ -121,7 +122,7 @@ These use the same attributes as [rustdoc attributes], with a few additions:
|
||||
* `no_run` --- The code is compiled when tested, but it is not run.
|
||||
The play button is also not shown.
|
||||
* `compile_fail` --- The code should fail to compile.
|
||||
* `edition2015`, `edition2018`, `edition2021` --- Forces the use of a specific Rust edition.
|
||||
* `edition2015`, `edition2018`, `edition2021`, `edition2024` --- Forces the use of a specific Rust edition.
|
||||
See [`rust.edition`] to set this globally.
|
||||
|
||||
[`mdbook test`]: ../cli/test.md
|
||||
|
||||
@@ -9,8 +9,8 @@ be added to the ***book.toml***:
|
||||
editable = true
|
||||
```
|
||||
|
||||
To make a specific block available for editing, the attribute `editable` needs
|
||||
to be added to it:
|
||||
After enabling editable code blocks, the `editable` attribute must be added to a
|
||||
code block to make it editable:
|
||||
|
||||
~~~markdown
|
||||
```rust,editable
|
||||
|
||||
@@ -99,3 +99,13 @@ Of course the inner html can be changed to your liking.
|
||||
|
||||
*If you would like other properties or helpers exposed, please [create a new
|
||||
issue](https://github.com/rust-lang/mdBook/issues)*
|
||||
|
||||
### 3. resource
|
||||
|
||||
The path to a static file.
|
||||
It implicitly includes `path_to_root`,
|
||||
and accounts for files that are renamed with a hash in their filename.
|
||||
|
||||
```handlebars
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ To make it easier to run, put the path to the binary into your `PATH`.
|
||||
|
||||
To build the `mdbook` executable from source, you will first need to install Rust and Cargo.
|
||||
Follow the instructions on the [Rust installation page].
|
||||
mdBook currently requires at least Rust version 1.74.
|
||||
mdBook currently requires at least Rust version 1.82.
|
||||
|
||||
Once you have installed Rust, the following command can be used to build and install mdBook:
|
||||
|
||||
|
||||
@@ -42,14 +42,14 @@ Tapping the menu bar will scroll the page to the top.
|
||||
## Search
|
||||
|
||||
Each book has a built-in search system.
|
||||
Pressing the search icon (<i class="fa fa-search"></i>) in the menu bar, or pressing the `S` key on the keyboard will open an input box for entering search terms.
|
||||
Pressing the search icon (<i class="fa fa-search"></i>) in the menu bar, or pressing the <kbd>/</kbd> or <kbd>S</kbd> key on the keyboard will open an input box for entering search terms.
|
||||
Typing some terms will show matching chapters and sections in real time.
|
||||
|
||||
Clicking any of the results will jump to that section.
|
||||
The up and down arrow keys can be used to navigate the results, and enter will open the highlighted section.
|
||||
|
||||
After loading a search result, the matching search terms will be highlighted in the text.
|
||||
Clicking a highlighted word or pressing the `Esc` key will remove the highlighting.
|
||||
Clicking a highlighted word or pressing the <kbd>Escape</kbd> key will remove the highlighting.
|
||||
|
||||
## Code blocks
|
||||
|
||||
|
||||
10
package.json
Normal file
10
package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"browser-ui-test": "0.21.1",
|
||||
"eslint": "^8.57.1"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/front-end/*js src/front-end/**/*js",
|
||||
"lint-fix": "eslint --fix src/front-end/*js src/front-end/**/*js"
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,8 @@ pub struct Chapter {
|
||||
/// `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.
|
||||
/// 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>,
|
||||
@@ -646,4 +647,19 @@ And here is some \
|
||||
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#"Couldn't open SUMMARY.md in {:?} directory"#,
|
||||
temp_dir.path()
|
||||
);
|
||||
assert_eq!(error_message, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
//!
|
||||
//! [1]: ../index.html
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod book;
|
||||
mod init;
|
||||
mod summary;
|
||||
@@ -346,7 +345,7 @@ impl MDBook {
|
||||
cmd.args(["--edition", "2021"]);
|
||||
}
|
||||
RustEdition::E2024 => {
|
||||
cmd.args(["--edition", "2024", "-Zunstable-options"]);
|
||||
cmd.args(["--edition", "2024"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,7 +355,9 @@ impl MDBook {
|
||||
}
|
||||
|
||||
debug!("running {:?}", cmd);
|
||||
let output = cmd.output()?;
|
||||
let output = cmd
|
||||
.output()
|
||||
.with_context(|| "failed to execute `rustdoc`")?;
|
||||
|
||||
if !output.status.success() {
|
||||
failed = true;
|
||||
@@ -858,7 +859,7 @@ mod tests {
|
||||
.and_then(Value::as_str)
|
||||
.unwrap();
|
||||
assert_eq!(html, "html");
|
||||
let html_renderer = HtmlHandlebars::default();
|
||||
let html_renderer = HtmlHandlebars;
|
||||
let pre = LinkPreprocessor::new();
|
||||
|
||||
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
|
||||
|
||||
@@ -3,6 +3,7 @@ use log::{debug, trace, warn};
|
||||
use memchr::Memchr;
|
||||
use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -245,6 +246,11 @@ impl<'a> SummaryParser<'a> {
|
||||
.parse_affix(false)
|
||||
.with_context(|| "There was an error parsing the suffix chapters")?;
|
||||
|
||||
let mut files = HashSet::new();
|
||||
for part in [&prefix_chapters, &numbered_chapters, &suffix_chapters] {
|
||||
Self::check_for_duplicates(&part, &mut files)?;
|
||||
}
|
||||
|
||||
Ok(Summary {
|
||||
title,
|
||||
prefix_chapters,
|
||||
@@ -253,6 +259,28 @@ impl<'a> SummaryParser<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Recursively check for duplicate files in the summary items.
|
||||
fn check_for_duplicates<'b>(
|
||||
items: &'b [SummaryItem],
|
||||
files: &mut HashSet<&'b PathBuf>,
|
||||
) -> Result<()> {
|
||||
for item in items {
|
||||
if let SummaryItem::Link(link) = item {
|
||||
if let Some(location) = &link.location {
|
||||
if !files.insert(location) {
|
||||
bail!(anyhow::anyhow!(
|
||||
"Duplicate file in SUMMARY.md: {:?}",
|
||||
location
|
||||
));
|
||||
}
|
||||
}
|
||||
// Recursively check nested items
|
||||
Self::check_for_duplicates(&link.nested_items, files)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse the affix chapters.
|
||||
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
|
||||
let mut items = Vec::new();
|
||||
@@ -747,6 +775,20 @@ mod tests {
|
||||
let got = parser.parse_affix(false);
|
||||
|
||||
assert!(got.is_err());
|
||||
let error_message = got.err().unwrap().to_string();
|
||||
assert_eq!(error_message, "failed to parse SUMMARY.md line 2, column 1: Suffix chapters cannot be followed by a list");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expected_a_start_of_a_link() {
|
||||
let src = "- Title\n";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
let got = parser.parse_affix(false);
|
||||
|
||||
assert!(got.is_err());
|
||||
let error_message = got.err().unwrap().to_string();
|
||||
assert_eq!(error_message, "failed to parse SUMMARY.md line 1, column 0: Suffix chapters cannot be followed by a list");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1113,4 +1155,83 @@ mod tests {
|
||||
let got = parser.parse_affix(false).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_entries_1() {
|
||||
let src = r#"
|
||||
# Summary
|
||||
- [A](./a.md)
|
||||
- [A](./a.md)
|
||||
"#;
|
||||
|
||||
let res = parse_summary(src);
|
||||
assert!(res.is_err());
|
||||
let error_message = res.err().unwrap().to_string();
|
||||
assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_entries_2() {
|
||||
let src = r#"
|
||||
# Summary
|
||||
- [A](./a.md)
|
||||
- [A](./a.md)
|
||||
"#;
|
||||
|
||||
let res = parse_summary(src);
|
||||
assert!(res.is_err());
|
||||
let error_message = res.err().unwrap().to_string();
|
||||
assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
|
||||
}
|
||||
#[test]
|
||||
fn duplicate_entries_3() {
|
||||
let src = r#"
|
||||
# Summary
|
||||
- [A](./a.md)
|
||||
- [B](./b.md)
|
||||
- [A](./a.md)
|
||||
"#;
|
||||
|
||||
let res = parse_summary(src);
|
||||
assert!(res.is_err());
|
||||
let error_message = res.err().unwrap().to_string();
|
||||
assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_entries_4() {
|
||||
let src = r#"
|
||||
# Summary
|
||||
[A](./a.md)
|
||||
- [B](./b.md)
|
||||
- [A](./a.md)
|
||||
"#;
|
||||
|
||||
let res = parse_summary(src);
|
||||
assert!(res.is_err());
|
||||
let error_message = res.err().unwrap().to_string();
|
||||
assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_entries_5() {
|
||||
let src = r#"
|
||||
# Summary
|
||||
[A](./a.md)
|
||||
|
||||
# hi
|
||||
- [B](./b.md)
|
||||
|
||||
# bye
|
||||
|
||||
---
|
||||
|
||||
[A](./a.md)
|
||||
"#;
|
||||
|
||||
let res = parse_summary(src);
|
||||
assert!(res.is_err());
|
||||
let error_message = res.err().unwrap().to_string();
|
||||
assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ use super::command_prelude::*;
|
||||
use crate::get_book_dir;
|
||||
use anyhow::Context;
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
use std::mem::take;
|
||||
use std::path::PathBuf;
|
||||
use std::{fmt, fs};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand() -> Command {
|
||||
@@ -23,10 +24,88 @@ pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
None => book.root.join(&book.config.build.build_dir),
|
||||
};
|
||||
|
||||
if dir_to_remove.exists() {
|
||||
fs::remove_dir_all(&dir_to_remove)
|
||||
.with_context(|| "Unable to remove the build directory")?;
|
||||
}
|
||||
let removed = Clean::new(&dir_to_remove)?;
|
||||
println!("{removed}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Formats a number of bytes into a human readable SI-prefixed size.
|
||||
/// Returns a tuple of `(quantity, units)`.
|
||||
pub fn human_readable_bytes(bytes: u64) -> (f32, &'static str) {
|
||||
static UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
||||
let bytes = bytes as f32;
|
||||
let i = ((bytes.log2() / 10.0) as usize).min(UNITS.len() - 1);
|
||||
(bytes / 1024_f32.powi(i as i32), UNITS[i])
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Clean {
|
||||
num_files_removed: u64,
|
||||
num_dirs_removed: u64,
|
||||
total_bytes_removed: u64,
|
||||
}
|
||||
|
||||
impl Clean {
|
||||
fn new(dir: &PathBuf) -> mdbook::errors::Result<Clean> {
|
||||
let mut files = vec![dir.clone()];
|
||||
let mut children = Vec::new();
|
||||
let mut num_files_removed = 0;
|
||||
let mut num_dirs_removed = 0;
|
||||
let mut total_bytes_removed = 0;
|
||||
|
||||
if dir.exists() {
|
||||
while !files.is_empty() {
|
||||
for file in files {
|
||||
if let Ok(meta) = file.metadata() {
|
||||
// Note: This can over-count bytes removed for hard-linked
|
||||
// files. It also under-counts since it only counts the exact
|
||||
// byte sizes and not the block sizes.
|
||||
total_bytes_removed += meta.len();
|
||||
}
|
||||
if file.is_file() {
|
||||
num_files_removed += 1;
|
||||
} else if file.is_dir() {
|
||||
num_dirs_removed += 1;
|
||||
for entry in fs::read_dir(file)? {
|
||||
children.push(entry?.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
files = take(&mut children);
|
||||
}
|
||||
fs::remove_dir_all(&dir).with_context(|| "Unable to remove the build directory")?;
|
||||
}
|
||||
|
||||
Ok(Clean {
|
||||
num_files_removed,
|
||||
num_dirs_removed,
|
||||
total_bytes_removed,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Clean {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Removed ")?;
|
||||
match (self.num_files_removed, self.num_dirs_removed) {
|
||||
(0, 0) => write!(f, "0 files")?,
|
||||
(0, 1) => write!(f, "1 directory")?,
|
||||
(0, 2..) => write!(f, "{} directories", self.num_dirs_removed)?,
|
||||
(1, _) => write!(f, "1 file")?,
|
||||
(2.., _) => write!(f, "{} files", self.num_files_removed)?,
|
||||
}
|
||||
|
||||
if self.total_bytes_removed == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
// Don't show a fractional number of bytes.
|
||||
if self.total_bytes_removed < 1024 {
|
||||
write!(f, ", {}B total", self.total_bytes_removed)
|
||||
} else {
|
||||
let (bytes, unit) = human_readable_bytes(self.total_bytes_removed);
|
||||
write!(f, ", {bytes:.2}{unit} total")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
if let Some(author) = get_author_name() {
|
||||
debug!("Obtained user name from gitconfig: {:?}", author);
|
||||
config.book.authors.push(author);
|
||||
builder.with_config(config);
|
||||
}
|
||||
|
||||
builder.with_config(config);
|
||||
builder.build()?;
|
||||
println!("\nAll done, no errors...");
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ use super::command_prelude::*;
|
||||
#[cfg(feature = "watch")]
|
||||
use super::watch;
|
||||
use crate::{get_book_dir, open};
|
||||
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use clap::builder::NonEmptyStringValueParser;
|
||||
use futures_util::sink::SinkExt;
|
||||
use futures_util::StreamExt;
|
||||
@@ -11,8 +14,7 @@ use mdbook::MDBook;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::broadcast;
|
||||
use warp::ws::Message;
|
||||
use warp::Filter;
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
|
||||
/// The HTTP endpoint for the websocket used to trigger reloads when a file changes.
|
||||
const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
|
||||
@@ -116,32 +118,19 @@ async fn serve(
|
||||
reload_tx: broadcast::Sender<Message>,
|
||||
file_404: &str,
|
||||
) {
|
||||
// A warp Filter which captures `reload_tx` and provides an `rx` copy to
|
||||
// receive reload messages.
|
||||
let sender = warp::any().map(move || reload_tx.subscribe());
|
||||
let reload_tx_clone = reload_tx.clone();
|
||||
|
||||
// A warp Filter to handle the livereload endpoint. This upgrades to a
|
||||
// websocket, and then waits for any filesystem change notifications, and
|
||||
// relays them over the websocket.
|
||||
let livereload = warp::path(LIVE_RELOAD_ENDPOINT)
|
||||
.and(warp::ws())
|
||||
.and(sender)
|
||||
.map(|ws: warp::ws::Ws, mut rx: broadcast::Receiver<Message>| {
|
||||
ws.on_upgrade(move |ws| async move {
|
||||
let (mut user_ws_tx, _user_ws_rx) = ws.split();
|
||||
trace!("websocket got connection");
|
||||
if let Ok(m) = rx.recv().await {
|
||||
trace!("notify of reload");
|
||||
let _ = user_ws_tx.send(m).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
// A warp Filter that serves from the filesystem.
|
||||
let book_route = warp::fs::dir(build_dir.clone());
|
||||
// The fallback route for 404 errors
|
||||
let fallback_route = warp::fs::file(build_dir.join(file_404))
|
||||
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
|
||||
let routes = livereload.or(book_route).or(fallback_route);
|
||||
// WebSocket handler for live reload
|
||||
let websocket_handler = move |ws: WebSocketUpgrade| async move {
|
||||
let reload_tx = reload_tx_clone.clone();
|
||||
ws.on_upgrade(move |socket| websocket_connection(socket, reload_tx))
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route(&format!("/{LIVE_RELOAD_ENDPOINT}"), get(websocket_handler))
|
||||
.fallback_service(
|
||||
ServeDir::new(&build_dir).not_found_service(ServeFile::new(build_dir.join(file_404))),
|
||||
);
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// exit if serve panics
|
||||
@@ -149,5 +138,20 @@ async fn serve(
|
||||
std::process::exit(1);
|
||||
}));
|
||||
|
||||
warp::serve(routes).run(address).await;
|
||||
let listener = tokio::net::TcpListener::bind(&address)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("Unable to bind to {address}: {e}"));
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn websocket_connection(ws: WebSocket, reload_tx: broadcast::Sender<Message>) {
|
||||
let (mut user_ws_tx, _user_ws_rx) = ws.split();
|
||||
let mut rx = reload_tx.subscribe();
|
||||
|
||||
trace!("websocket got connection");
|
||||
if let Ok(m) = rx.recv().await {
|
||||
trace!("notify of reload");
|
||||
let _ = user_ws_tx.send(m).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +312,8 @@ impl<'de> serde::Deserialize<'de> for Config {
|
||||
return Ok(Config::from_legacy(raw));
|
||||
}
|
||||
|
||||
warn_on_invalid_fields(&raw);
|
||||
|
||||
use serde::de::Error;
|
||||
let mut table = match raw {
|
||||
Value::Table(t) => t,
|
||||
@@ -376,6 +378,17 @@ fn parse_env(key: &str) -> Option<String> {
|
||||
.map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
|
||||
}
|
||||
|
||||
fn warn_on_invalid_fields(table: &Value) {
|
||||
let valid_items = ["book", "build", "rust", "output", "preprocessor"];
|
||||
|
||||
let table = table.as_table().expect("root must be a table");
|
||||
for item in table.keys() {
|
||||
if !valid_items.contains(&item.as_str()) {
|
||||
warn!("Invalid field {:?} in book.toml", &item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_legacy_format(table: &Value) -> bool {
|
||||
let legacy_items = [
|
||||
"title",
|
||||
@@ -408,6 +421,9 @@ pub struct BookConfig {
|
||||
/// Location of the book source relative to the book's root directory.
|
||||
pub src: PathBuf,
|
||||
/// Does this book support more than one language?
|
||||
// TODO: Remove this field in 0.5, it is unused:
|
||||
// https://github.com/rust-lang/mdBook/issues/2636
|
||||
#[serde(skip_serializing)]
|
||||
pub multilingual: bool,
|
||||
/// The main language of the book.
|
||||
pub language: Option<String>,
|
||||
@@ -587,6 +603,9 @@ pub struct HtmlConfig {
|
||||
/// The mapping from old pages to new pages/URLs to use when generating
|
||||
/// redirects.
|
||||
pub redirect: HashMap<String, String>,
|
||||
/// If this option is turned on, "cache bust" static files by adding
|
||||
/// hashes to their file names.
|
||||
pub hash_files: bool,
|
||||
}
|
||||
|
||||
impl Default for HtmlConfig {
|
||||
@@ -616,6 +635,7 @@ impl Default for HtmlConfig {
|
||||
cname: None,
|
||||
live_reload_endpoint: None,
|
||||
redirect: HashMap::new(),
|
||||
hash_files: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -735,6 +755,11 @@ pub struct Search {
|
||||
/// Copy JavaScript files for the search functionality to the output directory?
|
||||
/// Default: `true`.
|
||||
pub copy_js: bool,
|
||||
/// Specifies search settings for the given path.
|
||||
///
|
||||
/// The path can be for a specific chapter, or a directory. This will
|
||||
/// merge recursively, with more specific paths taking precedence.
|
||||
pub chapter: HashMap<String, SearchChapterSettings>,
|
||||
}
|
||||
|
||||
impl Default for Search {
|
||||
@@ -751,10 +776,19 @@ impl Default for Search {
|
||||
expand: true,
|
||||
heading_split_level: 3,
|
||||
copy_js: true,
|
||||
chapter: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Search options for chapters (or paths).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct SearchChapterSettings {
|
||||
/// Whether or not indexing is enabled, default `true`.
|
||||
pub enable: Option<bool>,
|
||||
}
|
||||
|
||||
/// Allows you to "update" any arbitrary field in a struct by round-tripping via
|
||||
/// a `toml::Value`.
|
||||
///
|
||||
|
||||
@@ -344,9 +344,34 @@ mark.fade-out {
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
|
||||
#searchbar-outer.searching #searchbar {
|
||||
padding-right: 30px;
|
||||
}
|
||||
#searchbar-outer .spinner-wrapper {
|
||||
display: none;
|
||||
}
|
||||
#searchbar-outer.searching .spinner-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spinner-wrapper {
|
||||
--spinner-margin: 2px;
|
||||
position: absolute;
|
||||
margin-block-start: calc(var(--searchbar-margin-block-start) + var(--spinner-margin));
|
||||
right: var(--spinner-margin);
|
||||
top: 0;
|
||||
bottom: var(--spinner-margin);
|
||||
padding: 6px;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
#searchbar {
|
||||
width: 100%;
|
||||
margin-block-start: 5px;
|
||||
margin-block-start: var(--searchbar-margin-block-start);
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
@@ -421,11 +446,14 @@ ul#searchresults span.teaser em {
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.sidebar-iframe-inner {
|
||||
--padding: 10px;
|
||||
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
padding: 10px 10px;
|
||||
padding: var(--padding);
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
color: var(--sidebar-fg);
|
||||
min-height: calc(100vh - var(--padding) * 2);
|
||||
}
|
||||
.sidebar-iframe-outer {
|
||||
border: none;
|
||||
@@ -471,9 +499,24 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
|
||||
.sidebar-resize-handle .sidebar-resize-indicator {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background-color: var(--icons);
|
||||
height: 16px;
|
||||
color: var(--icons);
|
||||
margin-inline-start: var(--sidebar-resize-indicator-space);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.sidebar-resize-handle .sidebar-resize-indicator::before {
|
||||
content: "";
|
||||
width: 2px;
|
||||
height: 12px;
|
||||
border-left: dotted 2px currentColor;
|
||||
}
|
||||
.sidebar-resize-handle .sidebar-resize-indicator::after {
|
||||
content: "";
|
||||
width: 2px;
|
||||
height: 16px;
|
||||
border-left: dotted 2px currentColor;
|
||||
}
|
||||
|
||||
[dir=rtl] .sidebar .sidebar-resize-handle {
|
||||
@@ -487,7 +530,6 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
/* sidebar-hidden */
|
||||
#sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
z-index: -1;
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
@@ -638,3 +680,46 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
margin-inline-start: -14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
/* The container for the help popup that covers the whole window. */
|
||||
#mdbook-help-container {
|
||||
/* Position and size for the whole window. */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* This uses flex layout (which is set in book.js), and centers the popup
|
||||
in the window.*/
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
/* Dim out the book while the popup is visible. */
|
||||
background: var(--overlay-bg);
|
||||
}
|
||||
|
||||
/* The popup help box. */
|
||||
#mdbook-help-popup {
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
border-width: 1px;
|
||||
border-color: var(--theme-popup-border);
|
||||
border-style: solid;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mdbook-help-title {
|
||||
text-align: center;
|
||||
/* mdbook's margin for h2 is way too large. */
|
||||
margin: 10px;
|
||||
}
|
||||
@@ -86,11 +86,12 @@ h6:target::before {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
.no-js .page-wrapper,
|
||||
html:not(.js) .page-wrapper,
|
||||
.js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
[dir=rtl] .js:not(.sidebar-resizing) .page-wrapper {
|
||||
[dir=rtl]:not(.js) .page-wrapper,
|
||||
[dir=rtl].js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
@@ -200,16 +201,53 @@ sup {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
:not(.footnote-definition) + .footnote-definition,
|
||||
.footnote-definition + :not(.footnote-definition) {
|
||||
margin-block-start: 2em;
|
||||
}
|
||||
.footnote-definition {
|
||||
font-size: 0.9em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.footnote-definition p {
|
||||
display: inline;
|
||||
/* The default spacing for a list is a little too large. */
|
||||
.footnote-definition ul,
|
||||
.footnote-definition ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.footnote-definition > li {
|
||||
/* Required to position the ::before target */
|
||||
position: relative;
|
||||
}
|
||||
.footnote-definition > li:target {
|
||||
scroll-margin-top: 50vh;
|
||||
}
|
||||
.footnote-reference:target {
|
||||
scroll-margin-top: 50vh;
|
||||
}
|
||||
/* Draws a border around the footnote (including the marker) when it is selected.
|
||||
TODO: If there are multiple linkbacks, highlight which one you just came
|
||||
from so you know which one to click.
|
||||
*/
|
||||
.footnote-definition > li:target::before {
|
||||
border: 2px solid var(--footnote-highlight);
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
bottom: -8px;
|
||||
left: -32px;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
}
|
||||
/* Pulses the footnote reference so you can quickly see where you left off reading.
|
||||
This could use some improvement.
|
||||
*/
|
||||
@media not (prefers-reduced-motion) {
|
||||
.footnote-reference:target {
|
||||
animation: fn-highlight 0.8s;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@keyframes fn-highlight {
|
||||
from {
|
||||
background-color: var(--footnote-highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltiptext {
|
||||
@@ -2,14 +2,16 @@
|
||||
/* Globals */
|
||||
|
||||
:root {
|
||||
--sidebar-width: 300px;
|
||||
--sidebar-target-width: 300px;
|
||||
--sidebar-width: min(var(--sidebar-target-width), 80vw);
|
||||
--sidebar-resize-indicator-width: 8px;
|
||||
--sidebar-resize-indicator-space: 2px;
|
||||
--page-padding: 15px;
|
||||
--content-max-width: 750px;
|
||||
--menu-bar-height: 50px;
|
||||
--mono-font: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
--code-font-size: 0.875em /* please adjust the ace font size accordingly in editor.js */
|
||||
--code-font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
--searchbar-margin-block-start: 5px;
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
@@ -61,6 +63,10 @@
|
||||
--copy-button-filter: invert(45%) sepia(6%) saturate(621%) hue-rotate(198deg) brightness(99%) contrast(85%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(68%) sepia(55%) saturate(531%) hue-rotate(341deg) brightness(104%) contrast(101%);
|
||||
|
||||
--footnote-highlight: #2668a6;
|
||||
|
||||
--overlay-bg: rgba(33, 40, 48, 0.4);
|
||||
}
|
||||
|
||||
.coal {
|
||||
@@ -110,6 +116,10 @@
|
||||
--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);
|
||||
}
|
||||
|
||||
.light, html:not(.js) {
|
||||
@@ -159,6 +169,10 @@
|
||||
--copy-button-filter: invert(45.49%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(14%) sepia(93%) saturate(4250%) hue-rotate(243deg) brightness(99%) contrast(130%);
|
||||
|
||||
--footnote-highlight: #7e7eff;
|
||||
|
||||
--overlay-bg: rgba(200, 200, 205, 0.4);
|
||||
}
|
||||
|
||||
.navy {
|
||||
@@ -208,6 +222,10 @@
|
||||
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(46%) sepia(20%) saturate(1537%) hue-rotate(156deg) brightness(85%) contrast(90%);
|
||||
|
||||
--footnote-highlight: #4079ae;
|
||||
|
||||
--overlay-bg: rgba(33, 40, 48, 0.4);
|
||||
}
|
||||
|
||||
.rust {
|
||||
@@ -255,6 +273,10 @@
|
||||
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(77%) sepia(16%) saturate(1798%) hue-rotate(328deg) brightness(98%) contrast(83%);
|
||||
|
||||
--footnote-highlight: #d3a17a;
|
||||
|
||||
--overlay-bg: rgba(150, 150, 150, 0.25);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 434 KiB |
@@ -7,7 +7,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'),
|
||||
url('open-sans-v17-all-charsets-300.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-300.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -16,7 +16,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
|
||||
url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-300italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -25,7 +25,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'),
|
||||
url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-regular.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -34,7 +34,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'),
|
||||
url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -43,7 +43,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
|
||||
url('open-sans-v17-all-charsets-600.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-600.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -52,7 +52,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-600italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -61,7 +61,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'),
|
||||
url('open-sans-v17-all-charsets-700.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-700.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -70,7 +70,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
|
||||
url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-700italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -79,7 +79,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
|
||||
url('open-sans-v17-all-charsets-800.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-800.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -88,7 +88,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-800italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -96,5 +96,5 @@
|
||||
font-family: 'Source Code Pro';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
|
||||
src: url('{{ resource "fonts/source-code-pro-v11-all-charsets-500.woff2" }}') format('woff2');
|
||||
}
|
||||
|
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 |
839
src/front-end/js/book.js
Normal file
839
src/front-end/js/book.js
Normal file
@@ -0,0 +1,839 @@
|
||||
'use strict';
|
||||
|
||||
/* global default_theme, default_dark_theme, default_light_theme, hljs, ClipboardJS */
|
||||
|
||||
// Fix back button cache problem
|
||||
window.onunload = function() { };
|
||||
|
||||
// Global variable, shared between modules
|
||||
function playground_text(playground, hidden = true) {
|
||||
const code_block = playground.querySelector('code');
|
||||
|
||||
if (window.ace && code_block.classList.contains('editable')) {
|
||||
const editor = window.ace.edit(code_block);
|
||||
return editor.getValue();
|
||||
} else if (hidden) {
|
||||
return code_block.textContent;
|
||||
} else {
|
||||
return code_block.innerText;
|
||||
}
|
||||
}
|
||||
|
||||
(function codeSnippets() {
|
||||
function fetch_with_timeout(url, options, timeout = 6000) {
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
|
||||
]);
|
||||
}
|
||||
|
||||
const playgrounds = Array.from(document.querySelectorAll('.playground'));
|
||||
if (playgrounds.length > 0) {
|
||||
fetch_with_timeout('https://play.rust-lang.org/meta/crates', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
// get list of crates available in the rust playground
|
||||
const playground_crates = response.crates.map(item => item['id']);
|
||||
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
|
||||
});
|
||||
}
|
||||
|
||||
function handle_crate_list_update(playground_block, playground_crates) {
|
||||
// update the play buttons after receiving the response
|
||||
update_play_button(playground_block, playground_crates);
|
||||
|
||||
// and install on change listener to dynamically update ACE editors
|
||||
if (window.ace) {
|
||||
const code_block = playground_block.querySelector('code');
|
||||
if (code_block.classList.contains('editable')) {
|
||||
const editor = window.ace.edit(code_block);
|
||||
editor.addEventListener('change', () => {
|
||||
update_play_button(playground_block, playground_crates);
|
||||
});
|
||||
// add Ctrl-Enter command to execute rust code
|
||||
editor.commands.addCommand({
|
||||
name: 'run',
|
||||
bindKey: {
|
||||
win: 'Ctrl-Enter',
|
||||
mac: 'Ctrl-Enter',
|
||||
},
|
||||
exec: _editor => run_rust_code(playground_block),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updates the visibility of play button based on `no_run` class and
|
||||
// used crates vs ones available on https://play.rust-lang.org
|
||||
function update_play_button(pre_block, playground_crates) {
|
||||
const play_button = pre_block.querySelector('.play-button');
|
||||
|
||||
// skip if code is `no_run`
|
||||
if (pre_block.querySelector('code').classList.contains('no_run')) {
|
||||
play_button.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// get list of `extern crate`'s from snippet
|
||||
const txt = playground_text(pre_block);
|
||||
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]);
|
||||
}
|
||||
|
||||
// check if all used crates are available on play.rust-lang.org
|
||||
const all_available = snippet_crates.every(function(elem) {
|
||||
return playground_crates.indexOf(elem) > -1;
|
||||
});
|
||||
|
||||
if (all_available) {
|
||||
play_button.classList.remove('hidden');
|
||||
} else {
|
||||
play_button.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function run_rust_code(code_block) {
|
||||
let result_block = code_block.querySelector('.result');
|
||||
if (!result_block) {
|
||||
result_block = document.createElement('code');
|
||||
result_block.className = 'result hljs language-bash';
|
||||
|
||||
code_block.append(result_block);
|
||||
}
|
||||
|
||||
const text = playground_text(code_block);
|
||||
const classes = code_block.querySelector('code').classList;
|
||||
let edition = '2015';
|
||||
classes.forEach(className => {
|
||||
if (className.startsWith('edition')) {
|
||||
edition = className.slice(7);
|
||||
}
|
||||
});
|
||||
const params = {
|
||||
version: 'stable',
|
||||
optimize: '0',
|
||||
code: text,
|
||||
edition: edition,
|
||||
};
|
||||
|
||||
if (text.indexOf('#![feature') !== -1) {
|
||||
params.version = 'nightly';
|
||||
}
|
||||
|
||||
result_block.innerText = 'Running...';
|
||||
|
||||
fetch_with_timeout('https://play.rust-lang.org/evaluate.json', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.result.trim() === '') {
|
||||
result_block.innerText = 'No output';
|
||||
result_block.classList.add('result-no-output');
|
||||
} else {
|
||||
result_block.innerText = response.result;
|
||||
result_block.classList.remove('result-no-output');
|
||||
}
|
||||
})
|
||||
.catch(error => result_block.innerText = 'Playground Communication: ' + error.message);
|
||||
}
|
||||
|
||||
// Syntax highlighting Configuration
|
||||
hljs.configure({
|
||||
tabReplace: ' ', // 4 spaces
|
||||
languages: [], // Languages used for auto-detection
|
||||
});
|
||||
|
||||
const code_nodes = Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
// Don't highlight `inline code` blocks in headers.
|
||||
.filter(function(node) {
|
||||
return !node.parentElement.classList.contains('header');
|
||||
});
|
||||
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
code_nodes
|
||||
.filter(function(node) {
|
||||
return node.classList.contains('editable');
|
||||
})
|
||||
.forEach(function(block) {
|
||||
block.classList.remove('language-rust');
|
||||
});
|
||||
|
||||
code_nodes
|
||||
.filter(function(node) {
|
||||
return !node.classList.contains('editable');
|
||||
})
|
||||
.forEach(function(block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} else {
|
||||
code_nodes.forEach(function(block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
}
|
||||
|
||||
// Adding the hljs class gives code blocks the color css
|
||||
// even if highlighting doesn't apply
|
||||
code_nodes.forEach(function(block) {
|
||||
block.classList.add('hljs');
|
||||
});
|
||||
|
||||
Array.from(document.querySelectorAll('code.hljs')).forEach(function(block) {
|
||||
|
||||
const lines = Array.from(block.querySelectorAll('.boring'));
|
||||
// If no lines were hidden, return
|
||||
if (!lines.length) {
|
||||
return;
|
||||
}
|
||||
block.classList.add('hide-boring');
|
||||
|
||||
const buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
buttons.innerHTML = '<button class="fa fa-eye" title="Show hidden lines" \
|
||||
aria-label="Show hidden lines"></button>';
|
||||
|
||||
// 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);
|
||||
|
||||
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);
|
||||
|
||||
block.classList.add('hide-boring');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (window.playground_copyable) {
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function(block) {
|
||||
const pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playground')) {
|
||||
let buttons = pre_block.querySelector('.buttons');
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
const clipButton = document.createElement('button');
|
||||
clipButton.className = 'clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
|
||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process playground code blocks
|
||||
Array.from(document.querySelectorAll('.playground')).forEach(function(pre_block) {
|
||||
// Add play button
|
||||
let buttons = pre_block.querySelector('.buttons');
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
const runCodeButton = document.createElement('button');
|
||||
runCodeButton.className = 'fa fa-play play-button';
|
||||
runCodeButton.hidden = true;
|
||||
runCodeButton.title = 'Run this code';
|
||||
runCodeButton.setAttribute('aria-label', runCodeButton.title);
|
||||
|
||||
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
||||
runCodeButton.addEventListener('click', () => {
|
||||
run_rust_code(pre_block);
|
||||
});
|
||||
|
||||
if (window.playground_copyable) {
|
||||
const copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
|
||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||
}
|
||||
|
||||
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.title = 'Undo changes';
|
||||
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
|
||||
|
||||
buttons.insertBefore(undoChangesButton, buttons.firstChild);
|
||||
|
||||
undoChangesButton.addEventListener('click', function() {
|
||||
const editor = window.ace.edit(code_block);
|
||||
editor.setValue(editor.originalCode);
|
||||
editor.clearSelection();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function themes() {
|
||||
const html = document.querySelector('html');
|
||||
const themeToggleButton = document.getElementById('theme-toggle');
|
||||
const themePopup = document.getElementById('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'),
|
||||
};
|
||||
|
||||
function showThemes() {
|
||||
themePopup.style.display = 'block';
|
||||
themeToggleButton.setAttribute('aria-expanded', true);
|
||||
themePopup.querySelector('button#' + get_theme()).focus();
|
||||
}
|
||||
|
||||
function updateThemeSelected() {
|
||||
themePopup.querySelectorAll('.theme-selected').forEach(function(el) {
|
||||
el.classList.remove('theme-selected');
|
||||
});
|
||||
const selected = get_saved_theme() ?? 'default_theme';
|
||||
let element = themePopup.querySelector('button#' + selected);
|
||||
if (element === null) {
|
||||
// Fall back in case there is no "Default" item.
|
||||
element = themePopup.querySelector('button#' + get_theme());
|
||||
}
|
||||
element.classList.add('theme-selected');
|
||||
}
|
||||
|
||||
function hideThemes() {
|
||||
themePopup.style.display = 'none';
|
||||
themeToggleButton.setAttribute('aria-expanded', false);
|
||||
themeToggleButton.focus();
|
||||
}
|
||||
|
||||
function get_saved_theme() {
|
||||
let theme = null;
|
||||
try {
|
||||
theme = localStorage.getItem('mdbook-theme');
|
||||
} catch (e) {
|
||||
// ignore error.
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
function delete_saved_theme() {
|
||||
localStorage.removeItem('mdbook-theme');
|
||||
}
|
||||
|
||||
function get_theme() {
|
||||
const theme = get_saved_theme();
|
||||
if (theme === null || theme === undefined || !themeIds.includes(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.
|
||||
return default_theme;
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? default_dark_theme
|
||||
: default_light_theme;
|
||||
} else {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
||||
let previousTheme = default_theme;
|
||||
function set_theme(theme, store = true) {
|
||||
let ace_theme;
|
||||
|
||||
if (theme === 'coal' || theme === 'navy') {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = false;
|
||||
stylesheets.highlight.disabled = true;
|
||||
|
||||
ace_theme = 'ace/theme/tomorrow_night';
|
||||
} else if (theme === 'ayu') {
|
||||
stylesheets.ayuHighlight.disabled = false;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = true;
|
||||
ace_theme = 'ace/theme/tomorrow_night';
|
||||
} else {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = false;
|
||||
ace_theme = 'ace/theme/dawn';
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
|
||||
}, 1);
|
||||
|
||||
if (window.ace && window.editors) {
|
||||
window.editors.forEach(function(editor) {
|
||||
editor.setTheme(ace_theme);
|
||||
});
|
||||
}
|
||||
|
||||
if (store) {
|
||||
try {
|
||||
localStorage.setItem('mdbook-theme', theme);
|
||||
} catch (e) {
|
||||
// ignore error.
|
||||
}
|
||||
}
|
||||
|
||||
html.classList.remove(previousTheme);
|
||||
html.classList.add(theme);
|
||||
previousTheme = theme;
|
||||
updateThemeSelected();
|
||||
}
|
||||
|
||||
const query = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
query.onchange = function() {
|
||||
set_theme(get_theme(), false);
|
||||
};
|
||||
|
||||
// Set theme.
|
||||
set_theme(get_theme(), false);
|
||||
|
||||
themeToggleButton.addEventListener('click', function() {
|
||||
if (themePopup.style.display === 'block') {
|
||||
hideThemes();
|
||||
} else {
|
||||
showThemes();
|
||||
}
|
||||
});
|
||||
|
||||
themePopup.addEventListener('click', function(e) {
|
||||
let theme;
|
||||
if (e.target.className === 'theme') {
|
||||
theme = e.target.id;
|
||||
} else if (e.target.parentElement.className === 'theme') {
|
||||
theme = e.target.parentElement.id;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (theme === 'default_theme' || theme === null) {
|
||||
delete_saved_theme();
|
||||
set_theme(get_theme(), false);
|
||||
} else {
|
||||
set_theme(theme);
|
||||
}
|
||||
});
|
||||
|
||||
themePopup.addEventListener('focusout', function(e) {
|
||||
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
|
||||
if (!!e.relatedTarget &&
|
||||
!themeToggleButton.contains(e.relatedTarget) &&
|
||||
!themePopup.contains(e.relatedTarget)
|
||||
) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
// Should not be needed, but it works around an issue on macOS & iOS:
|
||||
// https://github.com/rust-lang/mdBook/issues/628
|
||||
document.addEventListener('click', function(e) {
|
||||
if (themePopup.style.display === 'block' &&
|
||||
!themeToggleButton.contains(e.target) &&
|
||||
!themePopup.contains(e.target)
|
||||
) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
if (!themePopup.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let li;
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
hideThemes();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
li = document.activeElement.parentElement;
|
||||
if (li && li.previousElementSibling) {
|
||||
li.previousElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
li = document.activeElement.parentElement;
|
||||
if (li && li.nextElementSibling) {
|
||||
li.nextElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:first-child button').focus();
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:last-child button').focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function sidebar() {
|
||||
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');
|
||||
let firstContact = null;
|
||||
|
||||
|
||||
/* Because we cannot change the `display` using only CSS after/before the transition, we
|
||||
need JS to do it. We change the display to prevent the browsers search to find text inside
|
||||
the collapsed sidebar. */
|
||||
if (!document.documentElement.classList.contains('sidebar-visible')) {
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
sidebar.addEventListener('transitionend', () => {
|
||||
/* We only change the display to "none" if we're collapsing the sidebar. */
|
||||
if (!sidebarCheckbox.checked) {
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
});
|
||||
sidebarToggleButton.addEventListener('click', () => {
|
||||
/* To allow the sidebar expansion animation, we first need to put back the display. */
|
||||
if (!sidebarCheckbox.checked) {
|
||||
sidebar.style.display = '';
|
||||
// Workaround for Safari skipping the animation when changing
|
||||
// `display` and a transform in the same event loop. This forces a
|
||||
// reflow after updating the display.
|
||||
sidebar.offsetHeight;
|
||||
}
|
||||
});
|
||||
|
||||
function showSidebar() {
|
||||
document.documentElement.classList.add('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', 0);
|
||||
});
|
||||
sidebarToggleButton.setAttribute('aria-expanded', true);
|
||||
sidebar.setAttribute('aria-hidden', false);
|
||||
try {
|
||||
localStorage.setItem('mdbook-sidebar', 'visible');
|
||||
} catch (e) {
|
||||
// Ignore error.
|
||||
}
|
||||
}
|
||||
|
||||
function hideSidebar() {
|
||||
document.documentElement.classList.remove('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', -1);
|
||||
});
|
||||
sidebarToggleButton.setAttribute('aria-expanded', false);
|
||||
sidebar.setAttribute('aria-hidden', true);
|
||||
try {
|
||||
localStorage.setItem('mdbook-sidebar', 'hidden');
|
||||
} catch (e) {
|
||||
// Ignore error.
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
sidebarCheckbox.addEventListener('change', function sidebarToggle() {
|
||||
if (sidebarCheckbox.checked) {
|
||||
const current_width = parseInt(
|
||||
document.documentElement.style.getPropertyValue('--sidebar-target-width'), 10);
|
||||
if (current_width < 150) {
|
||||
document.documentElement.style.setProperty('--sidebar-target-width', '150px');
|
||||
}
|
||||
showSidebar();
|
||||
} else {
|
||||
hideSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
|
||||
|
||||
function initResize() {
|
||||
window.addEventListener('mousemove', resize, false);
|
||||
window.addEventListener('mouseup', stopResize, false);
|
||||
document.documentElement.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
let pos = e.clientX - sidebar.offsetLeft;
|
||||
if (pos < 20) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (!document.documentElement.classList.contains('sidebar-visible')) {
|
||||
showSidebar();
|
||||
}
|
||||
pos = Math.min(pos, window.innerWidth - 100);
|
||||
document.documentElement.style.setProperty('--sidebar-target-width', pos + 'px');
|
||||
}
|
||||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize() {
|
||||
document.documentElement.classList.remove('sidebar-resizing');
|
||||
window.removeEventListener('mousemove', resize, false);
|
||||
window.removeEventListener('mouseup', stopResize, false);
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
firstContact = {
|
||||
x: e.touches[0].clientX,
|
||||
time: Date.now(),
|
||||
};
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchmove', function(e) {
|
||||
if (!firstContact) {
|
||||
return;
|
||||
}
|
||||
|
||||
const curX = e.touches[0].clientX;
|
||||
const xDiff = curX - firstContact.x,
|
||||
tDiff = Date.now() - firstContact.time;
|
||||
|
||||
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
|
||||
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300)) {
|
||||
showSidebar();
|
||||
} else if (xDiff < 0 && curX < 300) {
|
||||
hideSidebar();
|
||||
}
|
||||
|
||||
firstContact = null;
|
||||
}
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
(function chapterNavigation() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
if (window.search && window.search.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
const html = document.querySelector('html');
|
||||
|
||||
function next() {
|
||||
const nextButton = document.querySelector('.nav-chapters.next');
|
||||
if (nextButton) {
|
||||
window.location.href = nextButton.href;
|
||||
}
|
||||
}
|
||||
function prev() {
|
||||
const previousButton = document.querySelector('.nav-chapters.previous');
|
||||
if (previousButton) {
|
||||
window.location.href = previousButton.href;
|
||||
}
|
||||
}
|
||||
function showHelp() {
|
||||
const container = document.getElementById('mdbook-help-container');
|
||||
const overlay = document.getElementById('mdbook-help-popup');
|
||||
container.style.display = 'flex';
|
||||
|
||||
// Clicking outside the popup will dismiss it.
|
||||
const mouseHandler = event => {
|
||||
if (overlay.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
document.removeEventListener('mousedown', mouseHandler);
|
||||
hideHelp();
|
||||
};
|
||||
|
||||
// Pressing esc will dismiss the popup.
|
||||
const escapeKeyHandler = event => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
document.removeEventListener('keydown', escapeKeyHandler, true);
|
||||
hideHelp();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escapeKeyHandler, true);
|
||||
document.getElementById('mdbook-help-container')
|
||||
.addEventListener('mousedown', mouseHandler);
|
||||
}
|
||||
function hideHelp() {
|
||||
document.getElementById('mdbook-help-container').style.display = 'none';
|
||||
}
|
||||
|
||||
// Usually needs the Shift key to be pressed
|
||||
switch (e.key) {
|
||||
case '?':
|
||||
e.preventDefault();
|
||||
showHelp();
|
||||
break;
|
||||
}
|
||||
|
||||
// Rest of the keys are only active when the Shift key is not pressed
|
||||
if (e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
if (html.dir === 'rtl') {
|
||||
prev();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
if (html.dir === 'rtl') {
|
||||
next();
|
||||
} else {
|
||||
prev();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function clipboard() {
|
||||
const clipButtons = document.querySelectorAll('.clip-button');
|
||||
|
||||
function hideTooltip(elem) {
|
||||
elem.firstChild.innerText = '';
|
||||
elem.className = 'clip-button';
|
||||
}
|
||||
|
||||
function showTooltip(elem, msg) {
|
||||
elem.firstChild.innerText = msg;
|
||||
elem.className = 'clip-button tooltipped';
|
||||
}
|
||||
|
||||
const clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||
text: function(trigger) {
|
||||
hideTooltip(trigger);
|
||||
const playground = trigger.closest('pre');
|
||||
return playground_text(playground, false);
|
||||
},
|
||||
});
|
||||
|
||||
Array.from(clipButtons).forEach(function(clipButton) {
|
||||
clipButton.addEventListener('mouseout', function(e) {
|
||||
hideTooltip(e.currentTarget);
|
||||
});
|
||||
});
|
||||
|
||||
clipboardSnippets.on('success', function(e) {
|
||||
e.clearSelection();
|
||||
showTooltip(e.trigger, 'Copied!');
|
||||
});
|
||||
|
||||
clipboardSnippets.on('error', function(e) {
|
||||
showTooltip(e.trigger, 'Clipboard error!');
|
||||
});
|
||||
})();
|
||||
|
||||
(function scrollToTop() {
|
||||
const menuTitle = document.querySelector('.menu-title');
|
||||
|
||||
menuTitle.addEventListener('click', function() {
|
||||
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
})();
|
||||
|
||||
(function controllMenu() {
|
||||
const menu = document.getElementById('menu-bar');
|
||||
|
||||
(function controllPosition() {
|
||||
let scrollTop = document.scrollingElement.scrollTop;
|
||||
let prevScrollTop = scrollTop;
|
||||
const minMenuY = -menu.clientHeight - 50;
|
||||
// When the script loads, the page can be at any scroll (e.g. if you refresh it).
|
||||
menu.style.top = scrollTop + 'px';
|
||||
// Same as parseInt(menu.style.top.slice(0, -2), but faster
|
||||
let topCache = menu.style.top.slice(0, -2);
|
||||
menu.classList.remove('sticky');
|
||||
let stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
|
||||
document.addEventListener('scroll', function() {
|
||||
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
|
||||
// `null` means that it doesn't need to be updated
|
||||
let nextSticky = null;
|
||||
let nextTop = null;
|
||||
const scrollDown = scrollTop > prevScrollTop;
|
||||
const menuPosAbsoluteY = topCache - scrollTop;
|
||||
if (scrollDown) {
|
||||
nextSticky = false;
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextTop = prevScrollTop;
|
||||
}
|
||||
} else {
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextSticky = true;
|
||||
} else if (menuPosAbsoluteY < minMenuY) {
|
||||
nextTop = prevScrollTop + minMenuY;
|
||||
}
|
||||
}
|
||||
if (nextSticky === true && stickyCache === false) {
|
||||
menu.classList.add('sticky');
|
||||
stickyCache = true;
|
||||
} else if (nextSticky === false && stickyCache === true) {
|
||||
menu.classList.remove('sticky');
|
||||
stickyCache = false;
|
||||
}
|
||||
if (nextTop !== null) {
|
||||
menu.style.top = nextTop + 'px';
|
||||
topCache = nextTop;
|
||||
}
|
||||
prevScrollTop = scrollTop;
|
||||
}, { passive: true });
|
||||
})();
|
||||
(function controllBorder() {
|
||||
function updateBorder() {
|
||||
if (menu.offsetTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
} else {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
}
|
||||
updateBorder();
|
||||
document.addEventListener('scroll', updateBorder, { passive: true });
|
||||
})();
|
||||
})();
|
||||
@@ -13,32 +13,31 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::errors::*;
|
||||
use log::warn;
|
||||
pub static INDEX: &[u8] = include_bytes!("index.hbs");
|
||||
pub static HEAD: &[u8] = include_bytes!("head.hbs");
|
||||
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
|
||||
pub static HEADER: &[u8] = include_bytes!("header.hbs");
|
||||
pub static TOC_JS: &[u8] = include_bytes!("toc.js.hbs");
|
||||
pub static TOC_HTML: &[u8] = include_bytes!("toc.html.hbs");
|
||||
pub static INDEX: &[u8] = include_bytes!("templates/index.hbs");
|
||||
pub static HEAD: &[u8] = include_bytes!("templates/head.hbs");
|
||||
pub static REDIRECT: &[u8] = include_bytes!("templates/redirect.hbs");
|
||||
pub static HEADER: &[u8] = include_bytes!("templates/header.hbs");
|
||||
pub static TOC_JS: &[u8] = include_bytes!("templates/toc.js.hbs");
|
||||
pub static TOC_HTML: &[u8] = include_bytes!("templates/toc.html.hbs");
|
||||
pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
|
||||
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
|
||||
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
|
||||
pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css");
|
||||
pub static FAVICON_PNG: &[u8] = include_bytes!("favicon.png");
|
||||
pub static FAVICON_SVG: &[u8] = include_bytes!("favicon.svg");
|
||||
pub static JS: &[u8] = include_bytes!("book.js");
|
||||
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("highlight.js");
|
||||
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("tomorrow-night.css");
|
||||
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("highlight.css");
|
||||
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("ayu-highlight.css");
|
||||
pub static CLIPBOARD_JS: &[u8] = include_bytes!("clipboard.min.js");
|
||||
pub static FONT_AWESOME: &[u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &[u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2");
|
||||
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
|
||||
pub static FAVICON_PNG: &[u8] = include_bytes!("images/favicon.png");
|
||||
pub static FAVICON_SVG: &[u8] = include_bytes!("images/favicon.svg");
|
||||
pub static JS: &[u8] = include_bytes!("js/book.js");
|
||||
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("js/highlight.js");
|
||||
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("css/tomorrow-night.css");
|
||||
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("css/highlight.css");
|
||||
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("css/ayu-highlight.css");
|
||||
pub static CLIPBOARD_JS: &[u8] = include_bytes!("js/clipboard.min.js");
|
||||
pub static FONT_AWESOME: &[u8] = include_bytes!("css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &[u8] = include_bytes!("fonts/fontawesome-webfont.woff2");
|
||||
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("fonts/FontAwesome.otf");
|
||||
|
||||
/// The `Theme` struct should be used instead of the static variables because
|
||||
/// the `new()` method will look if the user has a theme directory in their
|
||||
554
src/front-end/searcher/searcher.js
Normal file
554
src/front-end/searcher/searcher.js
Normal file
@@ -0,0 +1,554 @@
|
||||
'use strict';
|
||||
|
||||
/* global Mark, elasticlunr, path_to_root */
|
||||
|
||||
window.search = window.search || {};
|
||||
(function search() {
|
||||
// Search functionality
|
||||
//
|
||||
// You can use !hasFocus() to prevent keyhandling in your key
|
||||
// event handlers while the user is typing their search.
|
||||
|
||||
if (!Mark || !elasticlunr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
// IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
||||
if (!String.prototype.startsWith) {
|
||||
String.prototype.startsWith = function(search, pos) {
|
||||
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === 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'),
|
||||
|
||||
// SVG text elements don't render if inside a <mark> tag.
|
||||
mark_exclude = ['text'],
|
||||
marker = new Mark(content),
|
||||
URL_SEARCH_PARAM = 'search',
|
||||
URL_MARK_PARAM = 'highlight';
|
||||
|
||||
let current_searchterm = '',
|
||||
doc_urls = [],
|
||||
search_options = {
|
||||
bool: 'AND',
|
||||
expand: true,
|
||||
fields: {
|
||||
title: {boost: 1},
|
||||
body: {boost: 1},
|
||||
breadcrumbs: {boost: 0},
|
||||
},
|
||||
},
|
||||
searchindex = null,
|
||||
results_options = {
|
||||
teaser_word_count: 30,
|
||||
limit_results: 30,
|
||||
},
|
||||
teaser_count = 0;
|
||||
|
||||
function hasFocus() {
|
||||
return searchbar === document.activeElement;
|
||||
}
|
||||
|
||||
function removeChildren(elem) {
|
||||
while (elem.firstChild) {
|
||||
elem.removeChild(elem.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to parse a url into its building blocks.
|
||||
function parseURL(url) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
return {
|
||||
source: url,
|
||||
protocol: a.protocol.replace(':', ''),
|
||||
host: a.hostname,
|
||||
port: a.port,
|
||||
params: (function() {
|
||||
const ret = {};
|
||||
const seg = a.search.replace(/^\?/, '').split('&');
|
||||
for (const part of seg) {
|
||||
if (!part) {
|
||||
continue;
|
||||
}
|
||||
const s = part.split('=');
|
||||
ret[s[0]] = s[1];
|
||||
}
|
||||
return ret;
|
||||
})(),
|
||||
file: (a.pathname.match(/\/([^/?#]+)$/i) || ['', ''])[1],
|
||||
hash: a.hash.replace('#', ''),
|
||||
path: a.pathname.replace(/^([^/])/, '/$1'),
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to recreate a url string from its building blocks.
|
||||
function renderURL(urlobject) {
|
||||
let url = urlobject.protocol + '://' + urlobject.host;
|
||||
if (urlobject.port !== '') {
|
||||
url += ':' + urlobject.port;
|
||||
}
|
||||
url += urlobject.path;
|
||||
let joiner = '?';
|
||||
for (const prop in urlobject.params) {
|
||||
if (Object.prototype.hasOwnProperty.call(urlobject.params, prop)) {
|
||||
url += joiner + prop + '=' + urlobject.params[prop];
|
||||
joiner = '&';
|
||||
}
|
||||
}
|
||||
if (urlobject.hash !== '') {
|
||||
url += '#' + urlobject.hash;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// Helper to escape html special chars for displaying the teasers
|
||||
const escapeHTML = (function() {
|
||||
const MAP = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
'\'': ''',
|
||||
};
|
||||
const repl = function(c) {
|
||||
return MAP[c];
|
||||
};
|
||||
return function(s) {
|
||||
return s.replace(/[&<>'"]/g, repl);
|
||||
};
|
||||
})();
|
||||
|
||||
function formatSearchMetric(count, searchterm) {
|
||||
if (count === 1) {
|
||||
return count + ' search result for \'' + searchterm + '\':';
|
||||
} else if (count === 0) {
|
||||
return 'No search results for \'' + searchterm + '\'.';
|
||||
} else {
|
||||
return count + ' search results for \'' + searchterm + '\':';
|
||||
}
|
||||
}
|
||||
|
||||
function formatSearchResult(result, searchterms) {
|
||||
const teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
|
||||
teaser_count++;
|
||||
|
||||
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
|
||||
const url = doc_urls[result.ref].split('#');
|
||||
if (url.length === 1) { // no anchor found
|
||||
url.push('');
|
||||
}
|
||||
|
||||
// encodeURIComponent escapes all chars that could allow an XSS except
|
||||
// for '. Due to that we also manually replace ' with its url-encoded
|
||||
// representation (%27).
|
||||
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
|
||||
+ '" aria-label="Search Result Teaser">' + teaser + '</span>';
|
||||
}
|
||||
|
||||
function makeTeaser(body, searchterms) {
|
||||
// The strategy is as follows:
|
||||
// First, assign a value to each word in the document:
|
||||
// Words that correspond to search terms (stemmer aware): 40
|
||||
// Normal words: 2
|
||||
// First word in a sentence: 8
|
||||
// Then use a sliding window with a constant number of words and count the
|
||||
// sum of the values of the words within the window. Then use the window that got the
|
||||
// maximum sum. If there are multiple maximas, then get the last one.
|
||||
// Enclose the terms in <em>.
|
||||
const stemmed_searchterms = searchterms.map(function(w) {
|
||||
return elasticlunr.stemmer(w.toLowerCase());
|
||||
});
|
||||
const searchterm_weight = 40;
|
||||
const weighted = []; // contains elements of ["word", weight, index_in_document]
|
||||
// split in sentences, then words
|
||||
const sentences = body.toLowerCase().split('. ');
|
||||
let index = 0;
|
||||
let value = 0;
|
||||
let searchterm_found = false;
|
||||
for (const sentenceindex in sentences) {
|
||||
const words = sentences[sentenceindex].split(' ');
|
||||
value = 8;
|
||||
for (const wordindex in words) {
|
||||
const word = words[wordindex];
|
||||
if (word.length > 0) {
|
||||
for (const searchtermindex in stemmed_searchterms) {
|
||||
if (elasticlunr.stemmer(word).startsWith(
|
||||
stemmed_searchterms[searchtermindex])
|
||||
) {
|
||||
value = searchterm_weight;
|
||||
searchterm_found = true;
|
||||
}
|
||||
}
|
||||
weighted.push([word, value, index]);
|
||||
value = 2;
|
||||
}
|
||||
index += word.length;
|
||||
index += 1; // ' ' or '.' if last word in sentence
|
||||
}
|
||||
index += 1; // because we split at a two-char boundary '. '
|
||||
}
|
||||
|
||||
if (weighted.length === 0) {
|
||||
return body;
|
||||
}
|
||||
|
||||
const window_weight = [];
|
||||
const window_size = Math.min(weighted.length, results_options.teaser_word_count);
|
||||
|
||||
let cur_sum = 0;
|
||||
for (let wordindex = 0; wordindex < window_size; wordindex++) {
|
||||
cur_sum += weighted[wordindex][1];
|
||||
}
|
||||
window_weight.push(cur_sum);
|
||||
for (let wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
|
||||
cur_sum -= weighted[wordindex][1];
|
||||
cur_sum += weighted[wordindex + window_size][1];
|
||||
window_weight.push(cur_sum);
|
||||
}
|
||||
|
||||
let max_sum_window_index = 0;
|
||||
if (searchterm_found) {
|
||||
let max_sum = 0;
|
||||
// backwards
|
||||
for (let i = window_weight.length - 1; i >= 0; i--) {
|
||||
if (window_weight[i] > max_sum) {
|
||||
max_sum = window_weight[i];
|
||||
max_sum_window_index = i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
max_sum_window_index = 0;
|
||||
}
|
||||
|
||||
// add <em/> around searchterms
|
||||
const teaser_split = [];
|
||||
index = weighted[max_sum_window_index][2];
|
||||
for (let i = max_sum_window_index; i < max_sum_window_index + window_size; i++) {
|
||||
const word = weighted[i];
|
||||
if (index < word[2]) {
|
||||
// missing text from index to start of `word`
|
||||
teaser_split.push(body.substring(index, word[2]));
|
||||
index = word[2];
|
||||
}
|
||||
if (word[1] === searchterm_weight) {
|
||||
teaser_split.push('<em>');
|
||||
}
|
||||
index = word[2] + word[0].length;
|
||||
teaser_split.push(body.substring(word[2], index));
|
||||
if (word[1] === searchterm_weight) {
|
||||
teaser_split.push('</em>');
|
||||
}
|
||||
}
|
||||
|
||||
return teaser_split.join('');
|
||||
}
|
||||
|
||||
function init(config) {
|
||||
results_options = config.results_options;
|
||||
search_options = config.search_options;
|
||||
doc_urls = config.doc_urls;
|
||||
searchindex = elasticlunr.Index.load(config.index);
|
||||
|
||||
searchbar_outer.classList.remove('searching');
|
||||
|
||||
searchbar.focus();
|
||||
|
||||
const searchterm = searchbar.value.trim();
|
||||
if (searchterm !== '') {
|
||||
searchbar.classList.add('active');
|
||||
doSearch(searchterm);
|
||||
}
|
||||
}
|
||||
|
||||
function initSearchInteractions(config) {
|
||||
// Set up events
|
||||
searchicon.addEventListener('click', () => {
|
||||
searchIconClickHandler();
|
||||
}, false);
|
||||
searchbar.addEventListener('keyup', () => {
|
||||
searchbarKeyUpHandler();
|
||||
}, false);
|
||||
document.addEventListener('keydown', e => {
|
||||
globalKeyHandler(e);
|
||||
}, false);
|
||||
// If the user uses the browser buttons, do the same as if a reload happened
|
||||
window.onpopstate = () => {
|
||||
doSearchOrMarkFromUrl();
|
||||
};
|
||||
// Suppress "submit" events so the page doesn't reload when the user presses Enter
|
||||
document.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
|
||||
// If reloaded, do the search or mark again, depending on the current url parameters
|
||||
doSearchOrMarkFromUrl();
|
||||
|
||||
// Exported functions
|
||||
config.hasFocus = hasFocus;
|
||||
}
|
||||
|
||||
initSearchInteractions(window.search);
|
||||
|
||||
function unfocusSearchbar() {
|
||||
// hacky, but just focusing a div only works once
|
||||
const tmp = document.createElement('input');
|
||||
tmp.setAttribute('style', 'position: absolute; opacity: 0;');
|
||||
searchicon.appendChild(tmp);
|
||||
tmp.focus();
|
||||
tmp.remove();
|
||||
}
|
||||
|
||||
// On reload or browser history backwards/forwards events, parse the url and do search or mark
|
||||
function doSearchOrMarkFromUrl() {
|
||||
// Check current URL for search request
|
||||
const url = parseURL(window.location.href);
|
||||
if (Object.prototype.hasOwnProperty.call(url.params, URL_SEARCH_PARAM)
|
||||
&& url.params[URL_SEARCH_PARAM] !== '') {
|
||||
showSearch(true);
|
||||
searchbar.value = decodeURIComponent(
|
||||
(url.params[URL_SEARCH_PARAM] + '').replace(/\+/g, '%20'));
|
||||
searchbarKeyUpHandler(); // -> doSearch()
|
||||
} else {
|
||||
showSearch(false);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(url.params, URL_MARK_PARAM)) {
|
||||
const words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
|
||||
marker.mark(words, {
|
||||
exclude: mark_exclude,
|
||||
});
|
||||
|
||||
const markers = document.querySelectorAll('mark');
|
||||
const hide = () => {
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
markers[i].classList.add('fade-out');
|
||||
window.setTimeout(() => {
|
||||
marker.unmark();
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
markers[i].addEventListener('click', hide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for keyevents on `document`
|
||||
function globalKeyHandler(e) {
|
||||
if (e.altKey ||
|
||||
e.ctrlKey ||
|
||||
e.metaKey ||
|
||||
e.shiftKey ||
|
||||
e.target.type === 'textarea' ||
|
||||
e.target.type === 'text' ||
|
||||
!hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
searchbar.classList.remove('active');
|
||||
setSearchUrlParameters('',
|
||||
searchbar.value.trim() !== '' ? 'push' : 'replace');
|
||||
if (hasFocus()) {
|
||||
unfocusSearchbar();
|
||||
}
|
||||
showSearch(false);
|
||||
marker.unmark();
|
||||
} else if (!hasFocus() && (e.key === 's' || e.key === '/')) {
|
||||
e.preventDefault();
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.select();
|
||||
} else if (hasFocus() && (e.key === 'ArrowDown'
|
||||
|| e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
const first = searchresults.firstElementChild;
|
||||
if (first !== null) {
|
||||
unfocusSearchbar();
|
||||
first.classList.add('focus');
|
||||
if (e.key === 'Enter') {
|
||||
window.location.assign(first.querySelector('a'));
|
||||
}
|
||||
}
|
||||
} else if (!hasFocus() && (e.key === 'ArrowDown'
|
||||
|| e.key === 'ArrowUp'
|
||||
|| e.key === 'Enter')) {
|
||||
// not `:focus` because browser does annoying scrolling
|
||||
const focused = searchresults.querySelector('li.focus');
|
||||
if (!focused) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (e.key === 'ArrowDown') {
|
||||
const next = focused.nextElementSibling;
|
||||
if (next) {
|
||||
focused.classList.remove('focus');
|
||||
next.classList.add('focus');
|
||||
}
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
focused.classList.remove('focus');
|
||||
const prev = focused.previousElementSibling;
|
||||
if (prev) {
|
||||
prev.classList.add('focus');
|
||||
} else {
|
||||
searchbar.select();
|
||||
}
|
||||
} else { // Enter
|
||||
window.location.assign(focused.querySelector('a'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadSearchScript(url, id) {
|
||||
if (document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
searchbar_outer.classList.add('searching');
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.id = id;
|
||||
script.onload = () => init(window.search);
|
||||
script.onerror = error => {
|
||||
console.error(`Failed to load \`${url}\`: ${error}`);
|
||||
};
|
||||
document.head.append(script);
|
||||
}
|
||||
|
||||
function showSearch(yes) {
|
||||
if (yes) {
|
||||
loadSearchScript(
|
||||
window.path_to_searchindex_js ||
|
||||
path_to_root + '{{ resource "searchindex.js" }}',
|
||||
'search-index');
|
||||
search_wrap.classList.remove('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
search_wrap.classList.add('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'false');
|
||||
const results = searchresults.children;
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
results[i].classList.remove('focus');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showResults(yes) {
|
||||
if (yes) {
|
||||
searchresults_outer.classList.remove('hidden');
|
||||
} else {
|
||||
searchresults_outer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for search icon
|
||||
function searchIconClickHandler() {
|
||||
if (search_wrap.classList.contains('hidden')) {
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.select();
|
||||
} else {
|
||||
showSearch(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for keyevents while the searchbar is focused
|
||||
function searchbarKeyUpHandler() {
|
||||
const searchterm = searchbar.value.trim();
|
||||
if (searchterm !== '') {
|
||||
searchbar.classList.add('active');
|
||||
doSearch(searchterm);
|
||||
} else {
|
||||
searchbar.classList.remove('active');
|
||||
showResults(false);
|
||||
removeChildren(searchresults);
|
||||
}
|
||||
|
||||
setSearchUrlParameters(searchterm, 'push_if_new_search_else_replace');
|
||||
|
||||
// Remove marks
|
||||
marker.unmark();
|
||||
}
|
||||
|
||||
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and
|
||||
// `#heading-anchor`. `action` can be one of "push", "replace",
|
||||
// "push_if_new_search_else_replace" and replaces or pushes a new browser history item.
|
||||
// "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
|
||||
function setSearchUrlParameters(searchterm, action) {
|
||||
const url = parseURL(window.location.href);
|
||||
const first_search = !Object.prototype.hasOwnProperty.call(url.params, URL_SEARCH_PARAM);
|
||||
|
||||
if (searchterm !== '' || action === 'push_if_new_search_else_replace') {
|
||||
url.params[URL_SEARCH_PARAM] = searchterm;
|
||||
delete url.params[URL_MARK_PARAM];
|
||||
url.hash = '';
|
||||
} else {
|
||||
delete url.params[URL_MARK_PARAM];
|
||||
delete url.params[URL_SEARCH_PARAM];
|
||||
}
|
||||
// A new search will also add a new history item, so the user can go back
|
||||
// to the page prior to searching. A updated search term will only replace
|
||||
// the url.
|
||||
if (action === 'push' || action === 'push_if_new_search_else_replace' && first_search ) {
|
||||
history.pushState({}, document.title, renderURL(url));
|
||||
} else if (action === 'replace' ||
|
||||
action === 'push_if_new_search_else_replace' &&
|
||||
!first_search
|
||||
) {
|
||||
history.replaceState({}, document.title, renderURL(url));
|
||||
}
|
||||
}
|
||||
|
||||
function doSearch(searchterm) {
|
||||
// Don't search the same twice
|
||||
if (current_searchterm === searchterm) {
|
||||
return;
|
||||
}
|
||||
searchbar_outer.classList.add('searching');
|
||||
if (searchindex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
current_searchterm = searchterm;
|
||||
|
||||
// Do the actual search
|
||||
const results = searchindex.search(searchterm, search_options);
|
||||
const resultcount = Math.min(results.length, results_options.limit_results);
|
||||
|
||||
// Display search metrics
|
||||
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
|
||||
|
||||
// Clear and insert results
|
||||
const searchterms = searchterm.split(' ');
|
||||
removeChildren(searchresults);
|
||||
for (let i = 0; i < resultcount ; i++) {
|
||||
const resultElem = document.createElement('li');
|
||||
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
|
||||
searchresults.appendChild(resultElem);
|
||||
}
|
||||
|
||||
// Display results
|
||||
showResults(true);
|
||||
searchbar_outer.classList.remove('searching');
|
||||
}
|
||||
|
||||
// Exported functions
|
||||
search.hasFocus = hasFocus;
|
||||
})(window.search);
|
||||
@@ -20,52 +20,71 @@
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
{{#if favicon_svg}}
|
||||
<link rel="icon" href="{{ path_to_root }}favicon.svg">
|
||||
<link rel="icon" href="{{ resource "favicon.svg" }}">
|
||||
{{/if}}
|
||||
{{#if favicon_png}}
|
||||
<link rel="shortcut icon" href="{{ path_to_root }}favicon.png">
|
||||
<link rel="shortcut icon" href="{{ resource "favicon.png" }}">
|
||||
{{/if}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
|
||||
{{/if}}
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="{{ resource "FontAwesome/css/font-awesome.css" }}">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
|
||||
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
|
||||
{{/if}}
|
||||
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}tomorrow-night.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}ayu-highlight.css">
|
||||
<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" }}">
|
||||
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
<link rel="stylesheet" href="{{ resource this }}">
|
||||
{{/each}}
|
||||
|
||||
{{#if mathjax_support}}
|
||||
<!-- MathJax -->
|
||||
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
|
||||
<!-- Provide site root and default themes to javascript -->
|
||||
<script>
|
||||
const path_to_root = "{{ path_to_root }}";
|
||||
const default_light_theme = "{{ default_theme }}";
|
||||
const default_dark_theme = "{{ preferred_dark_theme }}";
|
||||
{{#if search_js}}
|
||||
window.path_to_searchindex_js = "{{ resource "searchindex.js" }}";
|
||||
{{/if}}
|
||||
</script>
|
||||
<!-- Start loading toc.js asap -->
|
||||
<script src="{{ resource "toc.js" }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mdbook-help-container">
|
||||
<div id="mdbook-help-popup">
|
||||
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
|
||||
<div>
|
||||
<p>Press <kbd>←</kbd> or <kbd>→</kbd> to navigate between chapters</p>
|
||||
{{#if search_enabled}}
|
||||
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
|
||||
{{/if}}
|
||||
<p>Press <kbd>?</kbd> to show this help</p>
|
||||
<p>Press <kbd>Esc</kbd> to hide this help</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
var path_to_root = "{{ path_to_root }}";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
|
||||
</script>
|
||||
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script>
|
||||
try {
|
||||
var theme = localStorage.getItem('mdbook-theme');
|
||||
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||
let theme = localStorage.getItem('mdbook-theme');
|
||||
let sidebar = localStorage.getItem('mdbook-sidebar');
|
||||
|
||||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||
@@ -79,7 +98,8 @@
|
||||
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script>
|
||||
var theme;
|
||||
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
|
||||
let theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||
const html = document.documentElement;
|
||||
@@ -92,22 +112,25 @@
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var sidebar = null;
|
||||
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||||
let sidebar = null;
|
||||
const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||
sidebar = sidebar || 'visible';
|
||||
} else {
|
||||
sidebar = 'hidden';
|
||||
sidebar_toggle.checked = false;
|
||||
}
|
||||
if (sidebar === 'visible') {
|
||||
sidebar_toggle.checked = true;
|
||||
} else {
|
||||
html.classList.remove('sidebar-visible');
|
||||
}
|
||||
sidebar_toggle.checked = sidebar === 'visible';
|
||||
html.classList.remove('sidebar-visible');
|
||||
html.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<!-- populated by js -->
|
||||
<div class="sidebar-scrollbox"></div>
|
||||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
||||
<noscript>
|
||||
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
|
||||
</noscript>
|
||||
@@ -116,8 +139,6 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script async src="{{ path_to_root }}toc.js"></script>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
@@ -132,6 +153,7 @@
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="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>
|
||||
@@ -139,7 +161,7 @@
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
<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>
|
||||
{{/if}}
|
||||
@@ -159,7 +181,7 @@
|
||||
</a>
|
||||
{{/if}}
|
||||
{{#if git_repository_edit_url}}
|
||||
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit">
|
||||
<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>
|
||||
</a>
|
||||
{{/if}}
|
||||
@@ -170,7 +192,12 @@
|
||||
{{#if search_enabled}}
|
||||
<div id="search-wrapper" class="hidden">
|
||||
<form id="searchbar-outer" class="searchbar-outer">
|
||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
<div class="search-wrapper">
|
||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
<div class="spinner-wrapper">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||
<div id="searchresults-header" class="searchresults-header"></div>
|
||||
@@ -251,7 +278,7 @@
|
||||
{{#if google_analytics}}
|
||||
<!-- Google Analytics Tag -->
|
||||
<script>
|
||||
var localAddrs = ["localhost", "127.0.0.1", ""];
|
||||
const localAddrs = ["localhost", "127.0.0.1", ""];
|
||||
|
||||
// make sure we don't activate google analytics if the developer is
|
||||
// inspecting the book locally...
|
||||
@@ -280,26 +307,26 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_js}}
|
||||
<script src="{{ path_to_root }}ace.js"></script>
|
||||
<script src="{{ path_to_root }}editor.js"></script>
|
||||
<script src="{{ path_to_root }}mode-rust.js"></script>
|
||||
<script src="{{ path_to_root }}theme-dawn.js"></script>
|
||||
<script src="{{ path_to_root }}theme-tomorrow_night.js"></script>
|
||||
<script src="{{ resource "ace.js" }}"></script>
|
||||
<script src="{{ resource "mode-rust.js" }}"></script>
|
||||
<script src="{{ resource "editor.js" }}"></script>
|
||||
<script src="{{ resource "theme-dawn.js" }}"></script>
|
||||
<script src="{{ resource "theme-tomorrow_night.js" }}"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if search_js}}
|
||||
<script src="{{ path_to_root }}elasticlunr.min.js"></script>
|
||||
<script src="{{ path_to_root }}mark.min.js"></script>
|
||||
<script src="{{ path_to_root }}searcher.js"></script>
|
||||
<script src="{{ resource "elasticlunr.min.js" }}"></script>
|
||||
<script src="{{ resource "mark.min.js" }}"></script>
|
||||
<script src="{{ resource "searcher.js" }}"></script>
|
||||
{{/if}}
|
||||
|
||||
<script src="{{ path_to_root }}clipboard.min.js"></script>
|
||||
<script src="{{ path_to_root }}highlight.js"></script>
|
||||
<script src="{{ path_to_root }}book.js"></script>
|
||||
<script src="{{ resource "clipboard.min.js" }}"></script>
|
||||
<script src="{{ resource "highlight.js" }}"></script>
|
||||
<script src="{{ resource "book.js" }}"></script>
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script src="{{ ../path_to_root }}{{this}}"></script>
|
||||
<script src="{{ resource this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
@@ -320,6 +347,21 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if fragment_map}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fragmentMap =
|
||||
{{{fragment_map}}}
|
||||
;
|
||||
const target = fragmentMap[window.location.hash];
|
||||
if (target) {
|
||||
let url = new URL(target, window.location.href);
|
||||
window.location.replace(url.href);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
36
src/front-end/templates/redirect.hbs
Normal file
36
src/front-end/templates/redirect.hbs
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0; URL={{url}}">
|
||||
<link rel="canonical" href="{{url}}">
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
|
||||
|
||||
<script>
|
||||
// This handles redirects that involve fragments.
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fragmentMap =
|
||||
{{{fragment_map}}}
|
||||
;
|
||||
const fragment = window.location.hash;
|
||||
if (fragment) {
|
||||
let redirectUrl = "{{url}}";
|
||||
const target = fragmentMap[fragment];
|
||||
if (target) {
|
||||
let url = new URL(target, window.location.href);
|
||||
redirectUrl = url.href;
|
||||
} else {
|
||||
let url = new URL(redirectUrl, window.location.href);
|
||||
url.hash = window.location.hash;
|
||||
redirectUrl = url.href;
|
||||
}
|
||||
window.location.replace(redirectUrl);
|
||||
}
|
||||
// else redirect handled by http-equiv
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -21,20 +21,20 @@
|
||||
{{> head}}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
|
||||
{{/if}}
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="{{ resource "FontAwesome/css/font-awesome.css" }}">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
|
||||
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
|
||||
{{/if}}
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
<link rel="stylesheet" href="{{ resource this }}">
|
||||
{{/each}}
|
||||
</head>
|
||||
<body class="sidebar-iframe-inner">
|
||||
70
src/front-end/templates/toc.js.hbs
Normal file
70
src/front-end/templates/toc.js.hbs
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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";
|
||||
}
|
||||
var links = Array.prototype.slice.call(this.querySelectorAll("a"));
|
||||
var l = links.length;
|
||||
for (var i = 0; i < l; ++i) {
|
||||
var link = links[i];
|
||||
var href = link.getAttribute("href");
|
||||
if (href && !href.startsWith("#") && !/^(?:[a-z+]+:)?\/\//.test(href)) {
|
||||
link.href = path_to_root + href;
|
||||
}
|
||||
// The "index" page is supposed to alias the first chapter in the book.
|
||||
if (link.href === current_page || (i === 0 && path_to_root === "" && current_page.endsWith("/index.html"))) {
|
||||
link.classList.add("active");
|
||||
var parent = link.parentElement;
|
||||
if (parent && parent.classList.contains("chapter-item")) {
|
||||
parent.classList.add("expanded");
|
||||
}
|
||||
while (parent) {
|
||||
if (parent.tagName === "LI" && parent.previousElementSibling) {
|
||||
if (parent.previousElementSibling.classList.contains("chapter-item")) {
|
||||
parent.previousElementSibling.classList.add("expanded");
|
||||
}
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Track and set sidebar scroll position
|
||||
this.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', this.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
this.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
// Toggle buttons
|
||||
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
|
||||
function toggleSection(ev) {
|
||||
ev.currentTarget.parentElement.classList.toggle('expanded');
|
||||
}
|
||||
Array.from(sidebarAnchorToggles).forEach(function (el) {
|
||||
el.addEventListener('click', toggleSection);
|
||||
});
|
||||
}
|
||||
}
|
||||
window.customElements.define("mdbook-sidebar-scrollbox", MDBookSidebarScrollbox);
|
||||
@@ -87,6 +87,7 @@ pub mod book;
|
||||
pub mod config;
|
||||
pub mod preprocess;
|
||||
pub mod renderer;
|
||||
#[path = "front-end/mod.rs"]
|
||||
pub mod theme;
|
||||
pub mod utils;
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
use std::{path::Path, sync::LazyLock};
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::errors::*;
|
||||
use log::warn;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||
/// `README.md` is the de facto index file in markdown-based documentation.
|
||||
@@ -68,7 +67,7 @@ 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: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)^readme$").unwrap());
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)^readme$").unwrap());
|
||||
|
||||
RE.is_match(
|
||||
path.as_ref()
|
||||
|
||||
@@ -7,11 +7,11 @@ use regex::{CaptureMatches, Captures, Regex};
|
||||
use std::fs;
|
||||
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use log::{error, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
@@ -19,9 +19,9 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
|
||||
///
|
||||
/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
|
||||
///. lines, or only between the specified anchors.
|
||||
/// lines, or only between the specified anchors.
|
||||
/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
|
||||
///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
|
||||
/// specified or the lines between specified anchors, and include the rest of the file behind `#`.
|
||||
/// This hides the lines from initial display but shows them when the reader expands the code
|
||||
/// block and provides them to Rustdoc for testing.
|
||||
/// - `{{# playground}}` - Insert runnable Rust files
|
||||
@@ -148,7 +148,6 @@ enum RangeOrAnchor {
|
||||
}
|
||||
|
||||
// A range of lines specified with some include directive.
|
||||
#[allow(clippy::enum_variant_names)] // The prefix can't be removed, and is meant to mirror the contained type
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum LineRange {
|
||||
Range(Range<usize>),
|
||||
@@ -410,7 +409,7 @@ impl<'a> Iterator for LinkIter<'a> {
|
||||
fn find_links(contents: &str) -> LinkIter<'_> {
|
||||
// lazily compute following regex
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
|
||||
static RE: Lazy<Regex> = Lazy::new(|| {
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
|
||||
@@ -2,8 +2,9 @@ use crate::book::{Book, BookItem};
|
||||
use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers;
|
||||
use crate::renderer::html_handlebars::StaticFiles;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::theme::{self, playground_editor, Theme};
|
||||
use crate::theme::{self, Theme};
|
||||
use crate::utils;
|
||||
|
||||
use std::borrow::Cow;
|
||||
@@ -11,18 +12,20 @@ use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::utils::fs::get_404_output_file;
|
||||
use handlebars::Handlebars;
|
||||
use log::{debug, trace, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Captures, Regex};
|
||||
use serde_json::json;
|
||||
|
||||
/// The HTML renderer for mdBook.
|
||||
#[derive(Default)]
|
||||
pub struct HtmlHandlebars;
|
||||
|
||||
impl HtmlHandlebars {
|
||||
/// Returns a new instance of [`HtmlHandlebars`].
|
||||
pub fn new() -> Self {
|
||||
HtmlHandlebars
|
||||
}
|
||||
@@ -108,6 +111,14 @@ impl HtmlHandlebars {
|
||||
.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)?),
|
||||
);
|
||||
}
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
@@ -206,7 +217,6 @@ impl HtmlHandlebars {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
fn post_process(
|
||||
&self,
|
||||
rendered: String,
|
||||
@@ -222,134 +232,6 @@ impl HtmlHandlebars {
|
||||
rendered
|
||||
}
|
||||
|
||||
fn copy_static_files(
|
||||
&self,
|
||||
destination: &Path,
|
||||
theme: &Theme,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<()> {
|
||||
use crate::utils::fs::write_file;
|
||||
|
||||
write_file(
|
||||
destination,
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
|
||||
}
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
write_file(destination, "css/general.css", &theme.general_css)?;
|
||||
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
|
||||
if html_config.print.enable {
|
||||
write_file(destination, "css/print.css", &theme.print_css)?;
|
||||
}
|
||||
write_file(destination, "css/variables.css", &theme.variables_css)?;
|
||||
if let Some(contents) = &theme.favicon_png {
|
||||
write_file(destination, "favicon.png", contents)?;
|
||||
}
|
||||
if let Some(contents) = &theme.favicon_svg {
|
||||
write_file(destination, "favicon.svg", contents)?;
|
||||
}
|
||||
write_file(destination, "highlight.css", &theme.highlight_css)?;
|
||||
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
write_file(destination, "highlight.js", &theme.highlight_js)?;
|
||||
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/css/font-awesome.css",
|
||||
theme::FONT_AWESOME,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
// Don't copy the stock fonts if the user has specified their own fonts to use.
|
||||
if html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
}
|
||||
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
}
|
||||
write_file(
|
||||
destination,
|
||||
theme::fonts::SOURCE_CODE_PRO.0,
|
||||
theme::fonts::SOURCE_CODE_PRO.1,
|
||||
)?;
|
||||
}
|
||||
if let Some(fonts_css) = &theme.fonts_css {
|
||||
if !fonts_css.is_empty() {
|
||||
write_file(destination, "fonts/fonts.css", fonts_css)?;
|
||||
}
|
||||
}
|
||||
if !html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
warn!(
|
||||
"output.html.copy-fonts is deprecated.\n\
|
||||
This book appears to have copy-fonts=false in book.toml without a fonts.css file.\n\
|
||||
Add an empty `theme/fonts/fonts.css` file to squelch this warning."
|
||||
);
|
||||
}
|
||||
for font_file in &theme.font_files {
|
||||
let contents = fs::read(font_file)?;
|
||||
let filename = font_file.file_name().unwrap();
|
||||
let filename = Path::new("fonts").join(filename);
|
||||
write_file(destination, filename, &contents)?;
|
||||
}
|
||||
|
||||
let playground_config = &html_config.playground;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playground_config.editable && playground_config.copy_js {
|
||||
// Load the editor
|
||||
write_file(destination, "editor.js", playground_editor::JS)?;
|
||||
write_file(destination, "ace.js", playground_editor::ACE_JS)?;
|
||||
write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-dawn.js",
|
||||
playground_editor::THEME_DAWN_JS,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-tomorrow_night.js",
|
||||
playground_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the context with data for this file
|
||||
fn configure_print_version(
|
||||
&self,
|
||||
@@ -381,43 +263,6 @@ impl HtmlHandlebars {
|
||||
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
|
||||
}
|
||||
|
||||
/// Copy across any additional CSS and JavaScript files which the book
|
||||
/// has been configured to use.
|
||||
fn copy_additional_css_and_js(
|
||||
&self,
|
||||
html: &HtmlConfig,
|
||||
root: &Path,
|
||||
destination: &Path,
|
||||
) -> Result<()> {
|
||||
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
|
||||
|
||||
debug!("Copying additional CSS and JS");
|
||||
|
||||
for custom_file in custom_files {
|
||||
let input_location = root.join(custom_file);
|
||||
let output_location = destination.join(custom_file);
|
||||
if let Some(parent) = output_location.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Unable to create {}", parent.display()))?;
|
||||
}
|
||||
debug!(
|
||||
"Copying {} -> {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
);
|
||||
|
||||
fs::copy(&input_location, &output_location).with_context(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirects(
|
||||
&self,
|
||||
root: &Path,
|
||||
@@ -429,15 +274,27 @@ impl HtmlHandlebars {
|
||||
}
|
||||
|
||||
log::debug!("Emitting redirects");
|
||||
let redirects = combine_fragment_redirects(redirects);
|
||||
|
||||
for (original, new) in redirects {
|
||||
log::debug!("Redirecting \"{}\" → \"{}\"", original, new);
|
||||
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);
|
||||
self.emit_redirect(handlebars, &filename, new)?;
|
||||
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."
|
||||
);
|
||||
}
|
||||
log::debug!("Redirecting \"{}\" → \"{}\"", original, dest);
|
||||
self.emit_redirect(handlebars, &filename, &dest, &fragment_map)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -448,23 +305,17 @@ impl HtmlHandlebars {
|
||||
handlebars: &Handlebars<'_>,
|
||||
original: &Path,
|
||||
destination: &str,
|
||||
fragment_map: &BTreeMap<String, String>,
|
||||
) -> Result<()> {
|
||||
if original.exists() {
|
||||
// sanity check to avoid accidentally overwriting a real file.
|
||||
let msg = format!(
|
||||
"Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?",
|
||||
original.display(),
|
||||
destination,
|
||||
);
|
||||
return Err(Error::msg(msg));
|
||||
}
|
||||
|
||||
if let Some(parent) = original.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
|
||||
}
|
||||
|
||||
let js_map = serde_json::to_string(fragment_map)?;
|
||||
|
||||
let ctx = json!({
|
||||
"fragment_map": js_map,
|
||||
"url": destination,
|
||||
});
|
||||
let f = File::create(original)?;
|
||||
@@ -544,6 +395,57 @@ impl Renderer for HtmlHandlebars {
|
||||
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 = crate::config::Search::default();
|
||||
let search = html_config.search.as_ref().unwrap_or(&default);
|
||||
if search.enable {
|
||||
super::search::create_files(&search, &mut static_files, &book)?;
|
||||
}
|
||||
}
|
||||
|
||||
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)?;
|
||||
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.html ✓");
|
||||
data.remove("path");
|
||||
data.remove("is_toc_html");
|
||||
}
|
||||
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
utils::fs::write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
|
||||
}
|
||||
|
||||
let mut is_index = true;
|
||||
for item in book.iter() {
|
||||
let ctx = RenderItemContext {
|
||||
@@ -588,33 +490,6 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Creating print.html ✓");
|
||||
}
|
||||
|
||||
debug!("Render toc");
|
||||
{
|
||||
let rendered_toc = handlebars.render("toc_js", &data)?;
|
||||
utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.js ✓");
|
||||
data.insert("is_toc_html".to_owned(), json!(true));
|
||||
let rendered_toc = handlebars.render("toc_html", &data)?;
|
||||
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.html ✓");
|
||||
data.remove("is_toc_html");
|
||||
}
|
||||
|
||||
debug!("Copy static files");
|
||||
self.copy_static_files(destination, &theme, &html_config)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
self.copy_additional_css_and_js(&html_config, &ctx.root, destination)
|
||||
.with_context(|| "Unable to copy across additional CSS and JS")?;
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
{
|
||||
let search = html_config.search.unwrap_or_default();
|
||||
if search.enable {
|
||||
super::search::create_files(&search, destination, book)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
|
||||
.context("Unable to emit redirects")?;
|
||||
|
||||
@@ -803,10 +678,10 @@ fn make_data(
|
||||
/// Goes through the rendered HTML, making sure all header tags have
|
||||
/// an anchor respectively so people can link to sections directly.
|
||||
fn build_header_links(html: &str) -> String {
|
||||
static BUILD_HEADER_LINKS: Lazy<Regex> = Lazy::new(|| {
|
||||
static BUILD_HEADER_LINKS: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r#"<h(\d)(?: id="([^"]+)")?(?: class="([^"]+)")?>(.*?)</h\d>"#).unwrap()
|
||||
});
|
||||
static IGNORE_CLASS: &[&str] = &["menu-title"];
|
||||
static IGNORE_CLASS: &[&str] = &["menu-title", "mdbook-help-title"];
|
||||
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
@@ -836,7 +711,7 @@ fn build_header_links(html: &str) -> String {
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Insert a sinle link into a header, making sure each link gets its own
|
||||
/// Insert a single link into a header, making sure each link gets its own
|
||||
/// unique ID by appending an auto-incremented number (if necessary).
|
||||
fn insert_link_into_header(
|
||||
level: usize,
|
||||
@@ -864,8 +739,8 @@ fn insert_link_into_header(
|
||||
// ```
|
||||
// This function replaces all commas by spaces in the code block classes
|
||||
fn fix_code_blocks(html: &str) -> String {
|
||||
static FIX_CODE_BLOCKS: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap());
|
||||
static FIX_CODE_BLOCKS: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap());
|
||||
|
||||
FIX_CODE_BLOCKS
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
@@ -878,8 +753,8 @@ fn fix_code_blocks(html: &str) -> String {
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
static CODE_BLOCK_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
|
||||
static CODE_BLOCK_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
|
||||
|
||||
fn add_playground_pre(
|
||||
html: &str,
|
||||
@@ -931,7 +806,7 @@ fn add_playground_pre(
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!("# #![allow(unused)]\n{attrs}#fn main() {{\n{code}#}}").into()
|
||||
format!("# #![allow(unused)]\n{attrs}# fn main() {{\n{code}# }}").into()
|
||||
};
|
||||
content
|
||||
}
|
||||
@@ -947,8 +822,10 @@ fn add_playground_pre(
|
||||
/// Modifies all `<code>` blocks to convert "hidden" lines and to wrap them in
|
||||
/// a `<span class="boring">`.
|
||||
fn hide_lines(html: &str, code_config: &Code) -> String {
|
||||
static LANGUAGE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\blanguage-(\w+)\b").unwrap());
|
||||
static HIDELINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\bhidelines=(\S+)").unwrap());
|
||||
static LANGUAGE_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\blanguage-(\w+)\b").unwrap());
|
||||
static HIDELINES_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\bhidelines=(\S+)").unwrap());
|
||||
|
||||
CODE_BLOCK_RE
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
@@ -989,7 +866,8 @@ fn hide_lines(html: &str, code_config: &Code) -> String {
|
||||
}
|
||||
|
||||
fn hide_lines_rust(content: &str) -> String {
|
||||
static BORING_LINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap());
|
||||
static BORING_LINES_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap());
|
||||
|
||||
let mut result = String::with_capacity(content.len());
|
||||
let mut lines = content.lines().peekable();
|
||||
@@ -1003,12 +881,9 @@ fn hide_lines_rust(content: &str) -> String {
|
||||
result += &caps[3];
|
||||
result += newline;
|
||||
continue;
|
||||
} else if &caps[2] != "!" && &caps[2] != "[" {
|
||||
} else if matches!(&caps[2], "" | " ") {
|
||||
result += "<span class=\"boring\">";
|
||||
result += &caps[1];
|
||||
if &caps[2] != " " {
|
||||
result += &caps[2];
|
||||
}
|
||||
result += &caps[3];
|
||||
result += newline;
|
||||
result += "</span>";
|
||||
@@ -1073,6 +948,62 @@ struct RenderItemContext<'a> {
|
||||
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()) {
|
||||
log::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.display().to_string().replace('\\', "/"));
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::TextDirection;
|
||||
@@ -1134,7 +1065,7 @@ mod tests {
|
||||
fn add_playground() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
|
||||
@@ -1164,7 +1095,7 @@ mod tests {
|
||||
fn add_playground_edition2015() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
@@ -1188,7 +1119,7 @@ mod tests {
|
||||
fn add_playground_edition2018() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
@@ -1212,7 +1143,7 @@ mod tests {
|
||||
fn add_playground_edition2021() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
@@ -1237,8 +1168,12 @@ mod tests {
|
||||
fn hide_lines_language_rust() {
|
||||
let inputs = [
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n# #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n# #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>",),
|
||||
// # must be followed by a space for a line to be hidden
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n#fn main() {\nx()\n#}</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n#fn main() {\nx()\n#}</code></pre>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod navigation;
|
||||
pub mod resources;
|
||||
pub mod theme;
|
||||
pub mod toc;
|
||||
|
||||
45
src/renderer/html_handlebars/helpers/resources.rs
Normal file
45
src/renderer/html_handlebars/helpers/resources.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::utils;
|
||||
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
};
|
||||
|
||||
// Handlebars helper to find filenames with hashes in them
|
||||
#[derive(Clone)]
|
||||
pub struct ResourceHelper {
|
||||
pub hash_map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl HelperDef for ResourceHelper {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
h: &Helper<'rc>,
|
||||
_r: &'reg Handlebars<'_>,
|
||||
ctx: &'rc Context,
|
||||
rc: &mut RenderContext<'reg, 'rc>,
|
||||
out: &mut dyn Output,
|
||||
) -> 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(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let base_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||
})?
|
||||
.replace("\"", "");
|
||||
|
||||
let path_to_root = utils::fs::path_to_root(&base_path);
|
||||
|
||||
out.write(&path_to_root)?;
|
||||
out.write(self.hash_map.get(param).map(|p| &p[..]).unwrap_or(¶m))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -107,8 +107,7 @@ impl HelperDef for RenderToc {
|
||||
}
|
||||
|
||||
// Link
|
||||
let path_exists: bool;
|
||||
match item.get("path") {
|
||||
let path_exists = match item.get("path") {
|
||||
Some(path) if !path.is_empty() => {
|
||||
out.write("<a href=\"")?;
|
||||
let tmp = Path::new(path)
|
||||
@@ -125,13 +124,13 @@ impl HelperDef for RenderToc {
|
||||
} else {
|
||||
"\">"
|
||||
})?;
|
||||
path_exists = true;
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
out.write("<div>")?;
|
||||
path_exists = false;
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !self.no_section_label {
|
||||
// Section does not necessarily exist
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(missing_docs)] // FIXME: Document this
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use elasticlunr::{Index, IndexBuilder};
|
||||
use once_cell::sync::Lazy;
|
||||
use pulldown_cmark::*;
|
||||
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::Search;
|
||||
use crate::book::{Book, BookItem, Chapter};
|
||||
use crate::config::{Search, SearchChapterSettings};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::StaticFiles;
|
||||
use crate::theme::searcher;
|
||||
use crate::utils;
|
||||
use log::{debug, warn};
|
||||
@@ -26,7 +27,11 @@ fn tokenize(text: &str) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// Creates all files required for search.
|
||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
||||
pub fn create_files(
|
||||
search_config: &Search,
|
||||
static_files: &mut StaticFiles,
|
||||
book: &Book,
|
||||
) -> Result<()> {
|
||||
let mut index = IndexBuilder::new()
|
||||
.add_field_with_tokenizer("title", Box::new(&tokenize))
|
||||
.add_field_with_tokenizer("body", Box::new(&tokenize))
|
||||
@@ -35,26 +40,43 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
|
||||
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
||||
|
||||
let chapter_configs = sort_search_config(&search_config.chapter);
|
||||
validate_chapter_config(&chapter_configs, book)?;
|
||||
|
||||
for item in book.iter() {
|
||||
render_item(&mut index, search_config, &mut doc_urls, item)?;
|
||||
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;
|
||||
}
|
||||
}
|
||||
render_item(&mut index, search_config, &mut doc_urls, chapter)?;
|
||||
}
|
||||
|
||||
let index = write_to_json(index, search_config, doc_urls)?;
|
||||
debug!("Writing search index ✓");
|
||||
if index.len() > 10_000_000 {
|
||||
warn!("searchindex.json is very large ({} bytes)", index.len());
|
||||
warn!("search index is very large ({} bytes)", index.len());
|
||||
}
|
||||
|
||||
if search_config.copy_js {
|
||||
utils::fs::write_file(destination, "searchindex.json", index.as_bytes())?;
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
static_files.add_builtin(
|
||||
"searchindex.js",
|
||||
format!("Object.assign(window.search, {index});").as_bytes(),
|
||||
)?;
|
||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
|
||||
// To reduce the size of the generated JSON by preventing all `"` characters to be
|
||||
// escaped, we instead surround the string with much less common `'` character.
|
||||
format!(
|
||||
"window.search = Object.assign(window.search, JSON.parse('{}'));",
|
||||
index.replace("\\", "\\\\").replace("'", "\\'")
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
static_files.add_builtin("searcher.js", searcher::JS);
|
||||
static_files.add_builtin("mark.min.js", searcher::MARK_JS);
|
||||
static_files.add_builtin("elasticlunr.min.js", searcher::ELASTICLUNR_JS);
|
||||
debug!("Copying search files ✓");
|
||||
}
|
||||
|
||||
@@ -100,13 +122,8 @@ fn render_item(
|
||||
index: &mut Index,
|
||||
search_config: &Search,
|
||||
doc_urls: &mut Vec<String>,
|
||||
item: &BookItem,
|
||||
chapter: &Chapter,
|
||||
) -> Result<()> {
|
||||
let chapter = match *item {
|
||||
BookItem::Chapter(ref ch) if !ch.is_draft_chapter() => ch,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let chapter_path = chapter
|
||||
.path
|
||||
.as_ref()
|
||||
@@ -297,7 +314,7 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
|
||||
}
|
||||
|
||||
fn clean_html(html: &str) -> String {
|
||||
static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
|
||||
static AMMONIA: LazyLock<ammonia::Builder<'static>> = LazyLock::new(|| {
|
||||
let mut clean_content = HashSet::new();
|
||||
clean_content.insert("script");
|
||||
clean_content.insert("style");
|
||||
@@ -313,3 +330,171 @@ fn clean_html(html: &str) -> String {
|
||||
});
|
||||
AMMONIA.clean(html).to_string()
|
||||
}
|
||||
|
||||
fn settings_path(ch: &Chapter) -> Option<&Path> {
|
||||
ch.source_path.as_deref().or_else(|| ch.path.as_deref())
|
||||
}
|
||||
|
||||
fn validate_chapter_config(
|
||||
chapter_configs: &[(PathBuf, SearchChapterSettings)],
|
||||
book: &Book,
|
||||
) -> Result<()> {
|
||||
for (path, _) in chapter_configs {
|
||||
let found = book
|
||||
.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));
|
||||
if !found {
|
||||
bail!(
|
||||
"[output.html.search.chapter] key `{}` does not match any chapter paths",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sort_search_config(
|
||||
map: &HashMap<String, SearchChapterSettings>,
|
||||
) -> Vec<(PathBuf, SearchChapterSettings)> {
|
||||
let mut settings: Vec<_> = map
|
||||
.iter()
|
||||
.map(|(key, value)| (PathBuf::from(key), value.clone()))
|
||||
.collect();
|
||||
// Note: This is case-sensitive, and assumes the author uses the same case
|
||||
// as the actual filename.
|
||||
settings.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
settings
|
||||
}
|
||||
|
||||
fn get_chapter_settings(
|
||||
chapter_configs: &[(PathBuf, SearchChapterSettings)],
|
||||
source_path: &Path,
|
||||
) -> SearchChapterSettings {
|
||||
let mut result = SearchChapterSettings::default();
|
||||
for (path, config) in chapter_configs {
|
||||
if source_path.starts_with(path) {
|
||||
result.enable = config.enable.or(result.enable);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chapter_settings_priority() {
|
||||
let cfg = r#"
|
||||
[output.html.search.chapter]
|
||||
"cli/watch.md" = { enable = true }
|
||||
"cli" = { enable = false }
|
||||
"cli/inner/foo.md" = { enable = false }
|
||||
"cli/inner" = { enable = true }
|
||||
"foo" = {} # Just to make sure empty table is allowed.
|
||||
"#;
|
||||
let cfg: crate::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 [
|
||||
("foo.md", None),
|
||||
("cli/watch.md", Some(true)),
|
||||
("cli/index.md", Some(false)),
|
||||
("cli/inner/index.md", Some(true)),
|
||||
("cli/inner/foo.md", Some(false)),
|
||||
] {
|
||||
assert_eq!(
|
||||
get_chapter_settings(&chapter_configs, Path::new(path)),
|
||||
SearchChapterSettings { enable }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_basic() {
|
||||
assert_eq!(tokenize("hello world"), vec!["hello", "world"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_with_hyphens() {
|
||||
assert_eq!(
|
||||
tokenize("hello-world test-case"),
|
||||
vec!["hello", "world", "test", "case"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_mixed_whitespace() {
|
||||
assert_eq!(
|
||||
tokenize("hello\tworld\ntest\r\ncase"),
|
||||
vec!["hello", "world", "test", "case"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_empty_string() {
|
||||
assert_eq!(tokenize(""), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_only_whitespace() {
|
||||
assert_eq!(tokenize(" \t\n "), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_case_normalization() {
|
||||
assert_eq!(tokenize("Hello WORLD Test"), vec!["hello", "world", "test"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_trim_whitespace() {
|
||||
assert_eq!(tokenize(" hello world "), vec!["hello", "world"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_long_words_filtered() {
|
||||
let long_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX + 1);
|
||||
let short_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX);
|
||||
let input = format!("{} hello {}", long_word, short_word);
|
||||
assert_eq!(tokenize(&input), vec!["hello", &short_word]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_max_length_word() {
|
||||
let max_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX);
|
||||
assert_eq!(tokenize(&max_word), vec![max_word]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_special_characters() {
|
||||
assert_eq!(
|
||||
tokenize("hello,world.test!case?"),
|
||||
vec!["hello,world.test!case?"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_unicode() {
|
||||
assert_eq!(
|
||||
tokenize("café naïve résumé"),
|
||||
vec!["café", "naïve", "résumé"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_unicode_rtl_hebre() {
|
||||
assert_eq!(tokenize("שלום עולם"), vec!["שלום", "עולם"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_numbers() {
|
||||
assert_eq!(
|
||||
tokenize("test123 456-789 hello"),
|
||||
vec!["test123", "456", "789", "hello"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
358
src/renderer/html_handlebars/static_files.rs
Normal file
358
src/renderer/html_handlebars/static_files.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
//! 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 std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Map static files to their final names and contents.
|
||||
///
|
||||
/// It performs [fingerprinting], if you call the `hash_files` method.
|
||||
/// If hash-files is turned off, then the files will not be renamed.
|
||||
/// It also writes files to their final destination, when `write_files` is called,
|
||||
/// 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 {
|
||||
static_files: Vec<StaticFile>,
|
||||
hash_map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
enum StaticFile {
|
||||
Builtin {
|
||||
data: Vec<u8>,
|
||||
filename: String,
|
||||
},
|
||||
Additional {
|
||||
input_location: PathBuf,
|
||||
filename: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl StaticFiles {
|
||||
pub fn new(theme: &Theme, html_config: &HtmlConfig, root: &Path) -> Result<StaticFiles> {
|
||||
let static_files = Vec::new();
|
||||
let mut this = StaticFiles {
|
||||
hash_map: HashMap::new(),
|
||||
static_files,
|
||||
};
|
||||
|
||||
this.add_builtin("book.js", &theme.js);
|
||||
this.add_builtin("css/general.css", &theme.general_css);
|
||||
this.add_builtin("css/chrome.css", &theme.chrome_css);
|
||||
if html_config.print.enable {
|
||||
this.add_builtin("css/print.css", &theme.print_css);
|
||||
}
|
||||
this.add_builtin("css/variables.css", &theme.variables_css);
|
||||
if let Some(contents) = &theme.favicon_png {
|
||||
this.add_builtin("favicon.png", contents);
|
||||
}
|
||||
if let Some(contents) = &theme.favicon_svg {
|
||||
this.add_builtin("favicon.svg", contents);
|
||||
}
|
||||
this.add_builtin("highlight.css", &theme.highlight_css);
|
||||
this.add_builtin("tomorrow-night.css", &theme.tomorrow_night_css);
|
||||
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() {
|
||||
this.add_builtin("fonts/fonts.css", theme::fonts::CSS);
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
this.add_builtin(file_name, contents);
|
||||
}
|
||||
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
|
||||
this.add_builtin(file_name, contents);
|
||||
}
|
||||
this.add_builtin(
|
||||
theme::fonts::SOURCE_CODE_PRO.0,
|
||||
theme::fonts::SOURCE_CODE_PRO.1,
|
||||
);
|
||||
} else if let Some(fonts_css) = &theme.fonts_css {
|
||||
if !fonts_css.is_empty() {
|
||||
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;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playground_config.editable && playground_config.copy_js {
|
||||
// Load the editor
|
||||
this.add_builtin("editor.js", playground_editor::JS);
|
||||
this.add_builtin("ace.js", playground_editor::ACE_JS);
|
||||
this.add_builtin("mode-rust.js", playground_editor::MODE_RUST_JS);
|
||||
this.add_builtin("theme-dawn.js", playground_editor::THEME_DAWN_JS);
|
||||
this.add_builtin(
|
||||
"theme-tomorrow_night.js",
|
||||
playground_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
);
|
||||
}
|
||||
|
||||
let custom_files = html_config
|
||||
.additional_css
|
||||
.iter()
|
||||
.chain(html_config.additional_js.iter());
|
||||
|
||||
for custom_file in custom_files {
|
||||
let input_location = root.join(custom_file);
|
||||
|
||||
this.static_files.push(StaticFile::Additional {
|
||||
input_location,
|
||||
filename: custom_file
|
||||
.to_str()
|
||||
.with_context(|| "resource file names must be valid utf8")?
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
for input_location in theme.font_files.iter().cloned() {
|
||||
let filename = Path::new("fonts")
|
||||
.join(input_location.file_name().unwrap())
|
||||
.to_str()
|
||||
.with_context(|| "resource file names must be valid utf8")?
|
||||
.to_owned();
|
||||
this.static_files.push(StaticFile::Additional {
|
||||
input_location,
|
||||
filename,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
pub fn add_builtin(&mut self, filename: &str, data: &[u8]) {
|
||||
self.static_files.push(StaticFile::Builtin {
|
||||
filename: filename.to_owned(),
|
||||
data: data.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Updates this [`StaticFiles`] to hash the contents for determining the
|
||||
/// filename for each resource.
|
||||
pub 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 {
|
||||
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/")
|
||||
{
|
||||
let hex = hex::encode(&Sha256::digest(data)[..4]);
|
||||
let new_filename = format!("{}-{}.{}", name, hex, suffix);
|
||||
self.hash_map.insert(filename.clone(), new_filename.clone());
|
||||
*filename = new_filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
StaticFile::Additional {
|
||||
ref mut filename,
|
||||
ref input_location,
|
||||
} => {
|
||||
let mut parts = filename.splitn(2, '.');
|
||||
let parts = parts.next().and_then(|p| Some((p, parts.next()?)));
|
||||
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 buf = vec![0; 1024];
|
||||
loop {
|
||||
let amt = input_file
|
||||
.read(&mut buf)
|
||||
.with_context(|| "read static file for hashing")?;
|
||||
if amt == 0 {
|
||||
break;
|
||||
};
|
||||
digest.update(&buf[..amt]);
|
||||
}
|
||||
let hex = hex::encode(&digest.finalize()[..4]);
|
||||
let new_filename = format!("{}-{}.{}", name, hex, suffix);
|
||||
self.hash_map.insert(filename.clone(), new_filename.clone());
|
||||
*filename = new_filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_files(self, destination: &Path) -> Result<ResourceHelper> {
|
||||
use crate::utils::fs::write_file;
|
||||
use regex::bytes::{Captures, Regex};
|
||||
// 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());
|
||||
fn replace_all<'a>(
|
||||
hash_map: &HashMap<String, String>,
|
||||
data: &'a [u8],
|
||||
filename: &str,
|
||||
) -> Cow<'a, [u8]> {
|
||||
RESOURCE.replace_all(data, move |captures: &Captures<'_>| {
|
||||
let name = captures
|
||||
.get(1)
|
||||
.expect("capture 1 in resource regex")
|
||||
.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);
|
||||
format!("{}{}", path_to_root, resource_filename)
|
||||
.as_bytes()
|
||||
.to_owned()
|
||||
})
|
||||
}
|
||||
for static_file in &self.static_files {
|
||||
match static_file {
|
||||
StaticFile::Builtin { filename, data } => {
|
||||
debug!("Writing builtin -> {}", filename);
|
||||
let data = if filename.ends_with(".css") || filename.ends_with(".js") {
|
||||
replace_all(&self.hash_map, data, filename)
|
||||
} else {
|
||||
Cow::Borrowed(&data[..])
|
||||
};
|
||||
write_file(destination, filename, &data)?;
|
||||
}
|
||||
StaticFile::Additional {
|
||||
ref input_location,
|
||||
ref filename,
|
||||
} => {
|
||||
let output_location = destination.join(filename);
|
||||
debug!(
|
||||
"Copying {} -> {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
);
|
||||
if let Some(parent) = output_location.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.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)?;
|
||||
} else {
|
||||
fs::copy(input_location, &output_location).with_context(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hash_map = self.hash_map;
|
||||
Ok(ResourceHelper { hash_map })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::HtmlConfig;
|
||||
use crate::theme::Theme;
|
||||
use crate::utils::fs::write_file;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_write_directive() {
|
||||
let theme = Theme {
|
||||
index: Vec::new(),
|
||||
head: Vec::new(),
|
||||
redirect: Vec::new(),
|
||||
header: Vec::new(),
|
||||
chrome_css: Vec::new(),
|
||||
general_css: Vec::new(),
|
||||
print_css: Vec::new(),
|
||||
variables_css: Vec::new(),
|
||||
favicon_png: Some(Vec::new()),
|
||||
favicon_svg: Some(Vec::new()),
|
||||
js: Vec::new(),
|
||||
highlight_css: Vec::new(),
|
||||
tomorrow_night_css: Vec::new(),
|
||||
ayu_highlight_css: Vec::new(),
|
||||
highlight_js: Vec::new(),
|
||||
clipboard_js: Vec::new(),
|
||||
toc_js: Vec::new(),
|
||||
toc_html: Vec::new(),
|
||||
fonts_css: None,
|
||||
font_files: Vec::new(),
|
||||
};
|
||||
let temp_dir = TempDir::with_prefix("mdbook-").unwrap();
|
||||
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,
|
||||
br#"{{ resource "book.js" }}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut static_files = StaticFiles::new(&theme, &html_config, temp_dir.path()).unwrap();
|
||||
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(
|
||||
temp_dir
|
||||
.path()
|
||||
.join("static-files-test-case-reference-635c9cdc.js"),
|
||||
)
|
||||
.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();
|
||||
assert_eq!("", book_js_content);
|
||||
}
|
||||
}
|
||||
@@ -1,701 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// Fix back button cache problem
|
||||
window.onunload = function () { };
|
||||
|
||||
// Global variable, shared between modules
|
||||
function playground_text(playground, hidden = true) {
|
||||
let code_block = playground.querySelector("code");
|
||||
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
let editor = window.ace.edit(code_block);
|
||||
return editor.getValue();
|
||||
} else if (hidden) {
|
||||
return code_block.textContent;
|
||||
} else {
|
||||
return code_block.innerText;
|
||||
}
|
||||
}
|
||||
|
||||
(function codeSnippets() {
|
||||
function fetch_with_timeout(url, options, timeout = 6000) {
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
|
||||
]);
|
||||
}
|
||||
|
||||
var playgrounds = Array.from(document.querySelectorAll(".playground"));
|
||||
if (playgrounds.length > 0) {
|
||||
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
// get list of crates available in the rust playground
|
||||
let playground_crates = response.crates.map(item => item["id"]);
|
||||
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
|
||||
});
|
||||
}
|
||||
|
||||
function handle_crate_list_update(playground_block, playground_crates) {
|
||||
// update the play buttons after receiving the response
|
||||
update_play_button(playground_block, playground_crates);
|
||||
|
||||
// and install on change listener to dynamically update ACE editors
|
||||
if (window.ace) {
|
||||
let code_block = playground_block.querySelector("code");
|
||||
if (code_block.classList.contains("editable")) {
|
||||
let editor = window.ace.edit(code_block);
|
||||
editor.addEventListener("change", function (e) {
|
||||
update_play_button(playground_block, playground_crates);
|
||||
});
|
||||
// add Ctrl-Enter command to execute rust code
|
||||
editor.commands.addCommand({
|
||||
name: "run",
|
||||
bindKey: {
|
||||
win: "Ctrl-Enter",
|
||||
mac: "Ctrl-Enter"
|
||||
},
|
||||
exec: _editor => run_rust_code(playground_block)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updates the visibility of play button based on `no_run` class and
|
||||
// used crates vs ones available on https://play.rust-lang.org
|
||||
function update_play_button(pre_block, playground_crates) {
|
||||
var play_button = pre_block.querySelector(".play-button");
|
||||
|
||||
// skip if code is `no_run`
|
||||
if (pre_block.querySelector('code').classList.contains("no_run")) {
|
||||
play_button.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
// get list of `extern crate`'s from snippet
|
||||
var txt = playground_text(pre_block);
|
||||
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
||||
var snippet_crates = [];
|
||||
var item;
|
||||
while (item = re.exec(txt)) {
|
||||
snippet_crates.push(item[1]);
|
||||
}
|
||||
|
||||
// check if all used crates are available on play.rust-lang.org
|
||||
var all_available = snippet_crates.every(function (elem) {
|
||||
return playground_crates.indexOf(elem) > -1;
|
||||
});
|
||||
|
||||
if (all_available) {
|
||||
play_button.classList.remove("hidden");
|
||||
} else {
|
||||
play_button.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function run_rust_code(code_block) {
|
||||
var result_block = code_block.querySelector(".result");
|
||||
if (!result_block) {
|
||||
result_block = document.createElement('code');
|
||||
result_block.className = 'result hljs language-bash';
|
||||
|
||||
code_block.append(result_block);
|
||||
}
|
||||
|
||||
let text = playground_text(code_block);
|
||||
let classes = code_block.querySelector('code').classList;
|
||||
let edition = "2015";
|
||||
if(classes.contains("edition2018")) {
|
||||
edition = "2018";
|
||||
} else if(classes.contains("edition2021")) {
|
||||
edition = "2021";
|
||||
}
|
||||
var params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
code: text,
|
||||
edition: edition
|
||||
};
|
||||
|
||||
if (text.indexOf("#![feature") !== -1) {
|
||||
params.version = "nightly";
|
||||
}
|
||||
|
||||
result_block.innerText = "Running...";
|
||||
|
||||
fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.result.trim() === '') {
|
||||
result_block.innerText = "No output";
|
||||
result_block.classList.add("result-no-output");
|
||||
} else {
|
||||
result_block.innerText = response.result;
|
||||
result_block.classList.remove("result-no-output");
|
||||
}
|
||||
})
|
||||
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
|
||||
}
|
||||
|
||||
// Syntax highlighting Configuration
|
||||
hljs.configure({
|
||||
tabReplace: ' ', // 4 spaces
|
||||
languages: [], // Languages used for auto-detection
|
||||
});
|
||||
|
||||
let code_nodes = Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
// Don't highlight `inline code` blocks in headers.
|
||||
.filter(function (node) {return !node.parentElement.classList.contains("header"); });
|
||||
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
code_nodes
|
||||
.filter(function (node) {return node.classList.contains("editable"); })
|
||||
.forEach(function (block) { block.classList.remove('language-rust'); });
|
||||
|
||||
code_nodes
|
||||
.filter(function (node) {return !node.classList.contains("editable"); })
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
} else {
|
||||
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
}
|
||||
|
||||
// Adding the hljs class gives code blocks the color css
|
||||
// even if highlighting doesn't apply
|
||||
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
|
||||
|
||||
Array.from(document.querySelectorAll("code.hljs")).forEach(function (block) {
|
||||
|
||||
var lines = Array.from(block.querySelectorAll('.boring'));
|
||||
// If no lines were hidden, return
|
||||
if (!lines.length) { return; }
|
||||
block.classList.add("hide-boring");
|
||||
|
||||
var buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
buttons.innerHTML = "<button class=\"fa fa-eye\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
|
||||
|
||||
// add expand button
|
||||
var pre_block = block.parentNode;
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
|
||||
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('fa-eye')) {
|
||||
e.target.classList.remove('fa-eye');
|
||||
e.target.classList.add('fa-eye-slash');
|
||||
e.target.title = 'Hide lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
block.classList.remove('hide-boring');
|
||||
} else if (e.target.classList.contains('fa-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);
|
||||
|
||||
block.classList.add('hide-boring');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (window.playground_copyable) {
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
|
||||
var pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playground')) {
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
clipButton.className = 'clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
|
||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process playground code blocks
|
||||
Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
|
||||
// Add play button
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
var runCodeButton = document.createElement('button');
|
||||
runCodeButton.className = 'fa fa-play play-button';
|
||||
runCodeButton.hidden = true;
|
||||
runCodeButton.title = 'Run this code';
|
||||
runCodeButton.setAttribute('aria-label', runCodeButton.title);
|
||||
|
||||
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
||||
runCodeButton.addEventListener('click', function (e) {
|
||||
run_rust_code(pre_block);
|
||||
});
|
||||
|
||||
if (window.playground_copyable) {
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
|
||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||
}
|
||||
|
||||
let code_block = pre_block.querySelector("code");
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
var undoChangesButton = document.createElement('button');
|
||||
undoChangesButton.className = 'fa fa-history reset-button';
|
||||
undoChangesButton.title = 'Undo changes';
|
||||
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
|
||||
|
||||
buttons.insertBefore(undoChangesButton, buttons.firstChild);
|
||||
|
||||
undoChangesButton.addEventListener('click', function () {
|
||||
let editor = window.ace.edit(code_block);
|
||||
editor.setValue(editor.originalCode);
|
||||
editor.clearSelection();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function themes() {
|
||||
var html = document.querySelector('html');
|
||||
var themeToggleButton = document.getElementById('theme-toggle');
|
||||
var themePopup = document.getElementById('theme-list');
|
||||
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
|
||||
var themeIds = [];
|
||||
themePopup.querySelectorAll('button.theme').forEach(function (el) {
|
||||
themeIds.push(el.id);
|
||||
});
|
||||
var stylesheets = {
|
||||
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
|
||||
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
|
||||
highlight: document.querySelector("[href$='highlight.css']"),
|
||||
};
|
||||
|
||||
function showThemes() {
|
||||
themePopup.style.display = 'block';
|
||||
themeToggleButton.setAttribute('aria-expanded', true);
|
||||
themePopup.querySelector("button#" + get_theme()).focus();
|
||||
}
|
||||
|
||||
function updateThemeSelected() {
|
||||
themePopup.querySelectorAll('.theme-selected').forEach(function (el) {
|
||||
el.classList.remove('theme-selected');
|
||||
});
|
||||
themePopup.querySelector("button#" + get_theme()).classList.add('theme-selected');
|
||||
}
|
||||
|
||||
function hideThemes() {
|
||||
themePopup.style.display = 'none';
|
||||
themeToggleButton.setAttribute('aria-expanded', false);
|
||||
themeToggleButton.focus();
|
||||
}
|
||||
|
||||
function get_theme() {
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||
if (theme === null || theme === undefined || !themeIds.includes(theme)) {
|
||||
return default_theme;
|
||||
} else {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
||||
function set_theme(theme, store = true) {
|
||||
let ace_theme;
|
||||
|
||||
if (theme == 'coal' || theme == 'navy') {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = false;
|
||||
stylesheets.highlight.disabled = true;
|
||||
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else if (theme == 'ayu') {
|
||||
stylesheets.ayuHighlight.disabled = false;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = true;
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = false;
|
||||
ace_theme = "ace/theme/dawn";
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
|
||||
}, 1);
|
||||
|
||||
if (window.ace && window.editors) {
|
||||
window.editors.forEach(function (editor) {
|
||||
editor.setTheme(ace_theme);
|
||||
});
|
||||
}
|
||||
|
||||
var previousTheme = get_theme();
|
||||
|
||||
if (store) {
|
||||
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
|
||||
}
|
||||
|
||||
html.classList.remove(previousTheme);
|
||||
html.classList.add(theme);
|
||||
updateThemeSelected();
|
||||
}
|
||||
|
||||
// Set theme
|
||||
var theme = get_theme();
|
||||
|
||||
set_theme(theme, false);
|
||||
|
||||
themeToggleButton.addEventListener('click', function () {
|
||||
if (themePopup.style.display === 'block') {
|
||||
hideThemes();
|
||||
} else {
|
||||
showThemes();
|
||||
}
|
||||
});
|
||||
|
||||
themePopup.addEventListener('click', function (e) {
|
||||
var theme;
|
||||
if (e.target.className === "theme") {
|
||||
theme = e.target.id;
|
||||
} else if (e.target.parentElement.className === "theme") {
|
||||
theme = e.target.parentElement.id;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
set_theme(theme);
|
||||
});
|
||||
|
||||
themePopup.addEventListener('focusout', function(e) {
|
||||
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
|
||||
if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
|
||||
document.addEventListener('click', function(e) {
|
||||
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (!themePopup.contains(e.target)) { return; }
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
hideThemes();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
var li = document.activeElement.parentElement;
|
||||
if (li && li.previousElementSibling) {
|
||||
li.previousElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
var li = document.activeElement.parentElement;
|
||||
if (li && li.nextElementSibling) {
|
||||
li.nextElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:first-child button').focus();
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:last-child button').focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function sidebar() {
|
||||
var body = document.querySelector("body");
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
|
||||
var firstContact = null;
|
||||
|
||||
function showSidebar() {
|
||||
body.classList.remove('sidebar-hidden')
|
||||
body.classList.add('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function (link) {
|
||||
link.setAttribute('tabIndex', 0);
|
||||
});
|
||||
sidebarToggleButton.setAttribute('aria-expanded', true);
|
||||
sidebar.setAttribute('aria-hidden', false);
|
||||
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
|
||||
}
|
||||
|
||||
|
||||
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
|
||||
|
||||
function toggleSection(ev) {
|
||||
ev.currentTarget.parentElement.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
Array.from(sidebarAnchorToggles).forEach(function (el) {
|
||||
el.addEventListener('click', toggleSection);
|
||||
});
|
||||
|
||||
function hideSidebar() {
|
||||
body.classList.remove('sidebar-visible')
|
||||
body.classList.add('sidebar-hidden');
|
||||
Array.from(sidebarLinks).forEach(function (link) {
|
||||
link.setAttribute('tabIndex', -1);
|
||||
});
|
||||
sidebarToggleButton.setAttribute('aria-expanded', false);
|
||||
sidebar.setAttribute('aria-hidden', true);
|
||||
try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { }
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
|
||||
if (body.classList.contains("sidebar-hidden")) {
|
||||
var current_width = parseInt(
|
||||
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
|
||||
if (current_width < 150) {
|
||||
document.documentElement.style.setProperty('--sidebar-width', '150px');
|
||||
}
|
||||
showSidebar();
|
||||
} else if (body.classList.contains("sidebar-visible")) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (getComputedStyle(sidebar)['transform'] === 'none') {
|
||||
hideSidebar();
|
||||
} else {
|
||||
showSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
|
||||
|
||||
function initResize(e) {
|
||||
window.addEventListener('mousemove', resize, false);
|
||||
window.addEventListener('mouseup', stopResize, false);
|
||||
body.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
var pos = (e.clientX - sidebar.offsetLeft);
|
||||
if (pos < 20) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (body.classList.contains("sidebar-hidden")) {
|
||||
showSidebar();
|
||||
}
|
||||
pos = Math.min(pos, window.innerWidth - 100);
|
||||
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
|
||||
}
|
||||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize(e) {
|
||||
body.classList.remove('sidebar-resizing');
|
||||
window.removeEventListener('mousemove', resize, false);
|
||||
window.removeEventListener('mouseup', stopResize, false);
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', function (e) {
|
||||
firstContact = {
|
||||
x: e.touches[0].clientX,
|
||||
time: Date.now()
|
||||
};
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchmove', function (e) {
|
||||
if (!firstContact)
|
||||
return;
|
||||
|
||||
var curX = e.touches[0].clientX;
|
||||
var xDiff = curX - firstContact.x,
|
||||
tDiff = Date.now() - firstContact.time;
|
||||
|
||||
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
|
||||
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300))
|
||||
showSidebar();
|
||||
else if (xDiff < 0 && curX < 300)
|
||||
hideSidebar();
|
||||
|
||||
firstContact = null;
|
||||
}
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
(function chapterNavigation() {
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (window.search && window.search.hasFocus()) { return; }
|
||||
var html = document.querySelector('html');
|
||||
|
||||
function next() {
|
||||
var nextButton = document.querySelector('.nav-chapters.next');
|
||||
if (nextButton) {
|
||||
window.location.href = nextButton.href;
|
||||
}
|
||||
}
|
||||
function prev() {
|
||||
var previousButton = document.querySelector('.nav-chapters.previous');
|
||||
if (previousButton) {
|
||||
window.location.href = previousButton.href;
|
||||
}
|
||||
}
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
if (html.dir == 'rtl') {
|
||||
prev();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
if (html.dir == 'rtl') {
|
||||
next();
|
||||
} else {
|
||||
prev();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function clipboard() {
|
||||
var clipButtons = document.querySelectorAll('.clip-button');
|
||||
|
||||
function hideTooltip(elem) {
|
||||
elem.firstChild.innerText = "";
|
||||
elem.className = 'clip-button';
|
||||
}
|
||||
|
||||
function showTooltip(elem, msg) {
|
||||
elem.firstChild.innerText = msg;
|
||||
elem.className = 'clip-button tooltipped';
|
||||
}
|
||||
|
||||
var clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||
text: function (trigger) {
|
||||
hideTooltip(trigger);
|
||||
let playground = trigger.closest("pre");
|
||||
return playground_text(playground, false);
|
||||
}
|
||||
});
|
||||
|
||||
Array.from(clipButtons).forEach(function (clipButton) {
|
||||
clipButton.addEventListener('mouseout', function (e) {
|
||||
hideTooltip(e.currentTarget);
|
||||
});
|
||||
});
|
||||
|
||||
clipboardSnippets.on('success', function (e) {
|
||||
e.clearSelection();
|
||||
showTooltip(e.trigger, "Copied!");
|
||||
});
|
||||
|
||||
clipboardSnippets.on('error', function (e) {
|
||||
showTooltip(e.trigger, "Clipboard error!");
|
||||
});
|
||||
})();
|
||||
|
||||
(function scrollToTop () {
|
||||
var menuTitle = document.querySelector('.menu-title');
|
||||
|
||||
menuTitle.addEventListener('click', function () {
|
||||
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
})();
|
||||
|
||||
(function controllMenu() {
|
||||
var menu = document.getElementById('menu-bar');
|
||||
|
||||
(function controllPosition() {
|
||||
var scrollTop = document.scrollingElement.scrollTop;
|
||||
var prevScrollTop = scrollTop;
|
||||
var minMenuY = -menu.clientHeight - 50;
|
||||
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
|
||||
menu.style.top = scrollTop + 'px';
|
||||
// Same as parseInt(menu.style.top.slice(0, -2), but faster
|
||||
var topCache = menu.style.top.slice(0, -2);
|
||||
menu.classList.remove('sticky');
|
||||
var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
|
||||
document.addEventListener('scroll', function () {
|
||||
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
|
||||
// `null` means that it doesn't need to be updated
|
||||
var nextSticky = null;
|
||||
var nextTop = null;
|
||||
var scrollDown = scrollTop > prevScrollTop;
|
||||
var menuPosAbsoluteY = topCache - scrollTop;
|
||||
if (scrollDown) {
|
||||
nextSticky = false;
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextTop = prevScrollTop;
|
||||
}
|
||||
} else {
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextSticky = true;
|
||||
} else if (menuPosAbsoluteY < minMenuY) {
|
||||
nextTop = prevScrollTop + minMenuY;
|
||||
}
|
||||
}
|
||||
if (nextSticky === true && stickyCache === false) {
|
||||
menu.classList.add('sticky');
|
||||
stickyCache = true;
|
||||
} else if (nextSticky === false && stickyCache === true) {
|
||||
menu.classList.remove('sticky');
|
||||
stickyCache = false;
|
||||
}
|
||||
if (nextTop !== null) {
|
||||
menu.style.top = nextTop + 'px';
|
||||
topCache = nextTop;
|
||||
}
|
||||
prevScrollTop = scrollTop;
|
||||
}, { passive: true });
|
||||
})();
|
||||
(function controllBorder() {
|
||||
function updateBorder() {
|
||||
if (menu.offsetTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
} else {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
}
|
||||
updateBorder();
|
||||
document.addEventListener('scroll', updateBorder, { passive: true });
|
||||
})();
|
||||
})();
|
||||
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0; URL={{url}}">
|
||||
<link rel="canonical" href="{{url}}">
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,483 +0,0 @@
|
||||
"use strict";
|
||||
window.search = window.search || {};
|
||||
(function search(search) {
|
||||
// Search functionality
|
||||
//
|
||||
// You can use !hasFocus() to prevent keyhandling in your key
|
||||
// event handlers while the user is typing their search.
|
||||
|
||||
if (!Mark || !elasticlunr) {
|
||||
return;
|
||||
}
|
||||
|
||||
//IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
||||
if (!String.prototype.startsWith) {
|
||||
String.prototype.startsWith = function(search, pos) {
|
||||
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
|
||||
};
|
||||
}
|
||||
|
||||
var search_wrap = document.getElementById('search-wrapper'),
|
||||
searchbar = document.getElementById('searchbar'),
|
||||
searchbar_outer = document.getElementById('searchbar-outer'),
|
||||
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'),
|
||||
|
||||
searchindex = null,
|
||||
doc_urls = [],
|
||||
results_options = {
|
||||
teaser_word_count: 30,
|
||||
limit_results: 30,
|
||||
},
|
||||
search_options = {
|
||||
bool: "AND",
|
||||
expand: true,
|
||||
fields: {
|
||||
title: {boost: 1},
|
||||
body: {boost: 1},
|
||||
breadcrumbs: {boost: 0}
|
||||
}
|
||||
},
|
||||
mark_exclude = [],
|
||||
marker = new Mark(content),
|
||||
current_searchterm = "",
|
||||
URL_SEARCH_PARAM = 'search',
|
||||
URL_MARK_PARAM = 'highlight',
|
||||
teaser_count = 0,
|
||||
|
||||
SEARCH_HOTKEY_KEYCODE = 83,
|
||||
ESCAPE_KEYCODE = 27,
|
||||
DOWN_KEYCODE = 40,
|
||||
UP_KEYCODE = 38,
|
||||
SELECT_KEYCODE = 13;
|
||||
|
||||
function hasFocus() {
|
||||
return searchbar === document.activeElement;
|
||||
}
|
||||
|
||||
function removeChildren(elem) {
|
||||
while (elem.firstChild) {
|
||||
elem.removeChild(elem.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to parse a url into its building blocks.
|
||||
function parseURL(url) {
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
return {
|
||||
source: url,
|
||||
protocol: a.protocol.replace(':',''),
|
||||
host: a.hostname,
|
||||
port: a.port,
|
||||
params: (function(){
|
||||
var ret = {};
|
||||
var seg = a.search.replace(/^\?/,'').split('&');
|
||||
var len = seg.length, i = 0, s;
|
||||
for (;i<len;i++) {
|
||||
if (!seg[i]) { continue; }
|
||||
s = seg[i].split('=');
|
||||
ret[s[0]] = s[1];
|
||||
}
|
||||
return ret;
|
||||
})(),
|
||||
file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
|
||||
hash: a.hash.replace('#',''),
|
||||
path: a.pathname.replace(/^([^/])/,'/$1')
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to recreate a url string from its building blocks.
|
||||
function renderURL(urlobject) {
|
||||
var url = urlobject.protocol + "://" + urlobject.host;
|
||||
if (urlobject.port != "") {
|
||||
url += ":" + urlobject.port;
|
||||
}
|
||||
url += urlobject.path;
|
||||
var joiner = "?";
|
||||
for(var prop in urlobject.params) {
|
||||
if(urlobject.params.hasOwnProperty(prop)) {
|
||||
url += joiner + prop + "=" + urlobject.params[prop];
|
||||
joiner = "&";
|
||||
}
|
||||
}
|
||||
if (urlobject.hash != "") {
|
||||
url += "#" + urlobject.hash;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// Helper to escape html special chars for displaying the teasers
|
||||
var escapeHTML = (function() {
|
||||
var MAP = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
var repl = function(c) { return MAP[c]; };
|
||||
return function(s) {
|
||||
return s.replace(/[&<>'"]/g, repl);
|
||||
};
|
||||
})();
|
||||
|
||||
function formatSearchMetric(count, searchterm) {
|
||||
if (count == 1) {
|
||||
return count + " search result for '" + searchterm + "':";
|
||||
} else if (count == 0) {
|
||||
return "No search results for '" + searchterm + "'.";
|
||||
} else {
|
||||
return count + " search results for '" + searchterm + "':";
|
||||
}
|
||||
}
|
||||
|
||||
function formatSearchResult(result, searchterms) {
|
||||
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
|
||||
teaser_count++;
|
||||
|
||||
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
|
||||
var url = doc_urls[result.ref].split("#");
|
||||
if (url.length == 1) { // no anchor found
|
||||
url.push("");
|
||||
}
|
||||
|
||||
// encodeURIComponent escapes all chars that could allow an XSS except
|
||||
// for '. Due to that we also manually replace ' with its url-encoded
|
||||
// representation (%27).
|
||||
var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27");
|
||||
|
||||
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
|
||||
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
|
||||
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
|
||||
+ teaser + '</span>';
|
||||
}
|
||||
|
||||
function makeTeaser(body, searchterms) {
|
||||
// The strategy is as follows:
|
||||
// First, assign a value to each word in the document:
|
||||
// Words that correspond to search terms (stemmer aware): 40
|
||||
// Normal words: 2
|
||||
// First word in a sentence: 8
|
||||
// Then use a sliding window with a constant number of words and count the
|
||||
// sum of the values of the words within the window. Then use the window that got the
|
||||
// maximum sum. If there are multiple maximas, then get the last one.
|
||||
// Enclose the terms in <em>.
|
||||
var stemmed_searchterms = searchterms.map(function(w) {
|
||||
return elasticlunr.stemmer(w.toLowerCase());
|
||||
});
|
||||
var searchterm_weight = 40;
|
||||
var weighted = []; // contains elements of ["word", weight, index_in_document]
|
||||
// split in sentences, then words
|
||||
var sentences = body.toLowerCase().split('. ');
|
||||
var index = 0;
|
||||
var value = 0;
|
||||
var searchterm_found = false;
|
||||
for (var sentenceindex in sentences) {
|
||||
var words = sentences[sentenceindex].split(' ');
|
||||
value = 8;
|
||||
for (var wordindex in words) {
|
||||
var word = words[wordindex];
|
||||
if (word.length > 0) {
|
||||
for (var searchtermindex in stemmed_searchterms) {
|
||||
if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) {
|
||||
value = searchterm_weight;
|
||||
searchterm_found = true;
|
||||
}
|
||||
};
|
||||
weighted.push([word, value, index]);
|
||||
value = 2;
|
||||
}
|
||||
index += word.length;
|
||||
index += 1; // ' ' or '.' if last word in sentence
|
||||
};
|
||||
index += 1; // because we split at a two-char boundary '. '
|
||||
};
|
||||
|
||||
if (weighted.length == 0) {
|
||||
return body;
|
||||
}
|
||||
|
||||
var window_weight = [];
|
||||
var window_size = Math.min(weighted.length, results_options.teaser_word_count);
|
||||
|
||||
var cur_sum = 0;
|
||||
for (var wordindex = 0; wordindex < window_size; wordindex++) {
|
||||
cur_sum += weighted[wordindex][1];
|
||||
};
|
||||
window_weight.push(cur_sum);
|
||||
for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
|
||||
cur_sum -= weighted[wordindex][1];
|
||||
cur_sum += weighted[wordindex + window_size][1];
|
||||
window_weight.push(cur_sum);
|
||||
};
|
||||
|
||||
if (searchterm_found) {
|
||||
var max_sum = 0;
|
||||
var max_sum_window_index = 0;
|
||||
// backwards
|
||||
for (var i = window_weight.length - 1; i >= 0; i--) {
|
||||
if (window_weight[i] > max_sum) {
|
||||
max_sum = window_weight[i];
|
||||
max_sum_window_index = i;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
max_sum_window_index = 0;
|
||||
}
|
||||
|
||||
// add <em/> around searchterms
|
||||
var teaser_split = [];
|
||||
var index = weighted[max_sum_window_index][2];
|
||||
for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) {
|
||||
var word = weighted[i];
|
||||
if (index < word[2]) {
|
||||
// missing text from index to start of `word`
|
||||
teaser_split.push(body.substring(index, word[2]));
|
||||
index = word[2];
|
||||
}
|
||||
if (word[1] == searchterm_weight) {
|
||||
teaser_split.push("<em>")
|
||||
}
|
||||
index = word[2] + word[0].length;
|
||||
teaser_split.push(body.substring(word[2], index));
|
||||
if (word[1] == searchterm_weight) {
|
||||
teaser_split.push("</em>")
|
||||
}
|
||||
};
|
||||
|
||||
return teaser_split.join('');
|
||||
}
|
||||
|
||||
function init(config) {
|
||||
results_options = config.results_options;
|
||||
search_options = config.search_options;
|
||||
searchbar_outer = config.searchbar_outer;
|
||||
doc_urls = config.doc_urls;
|
||||
searchindex = elasticlunr.Index.load(config.index);
|
||||
|
||||
// Set up events
|
||||
searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
|
||||
searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false);
|
||||
document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false);
|
||||
// If the user uses the browser buttons, do the same as if a reload happened
|
||||
window.onpopstate = function(e) { doSearchOrMarkFromUrl(); };
|
||||
// Suppress "submit" events so the page doesn't reload when the user presses Enter
|
||||
document.addEventListener('submit', function(e) { e.preventDefault(); }, false);
|
||||
|
||||
// If reloaded, do the search or mark again, depending on the current url parameters
|
||||
doSearchOrMarkFromUrl();
|
||||
}
|
||||
|
||||
function unfocusSearchbar() {
|
||||
// hacky, but just focusing a div only works once
|
||||
var tmp = document.createElement('input');
|
||||
tmp.setAttribute('style', 'position: absolute; opacity: 0;');
|
||||
searchicon.appendChild(tmp);
|
||||
tmp.focus();
|
||||
tmp.remove();
|
||||
}
|
||||
|
||||
// On reload or browser history backwards/forwards events, parse the url and do search or mark
|
||||
function doSearchOrMarkFromUrl() {
|
||||
// Check current URL for search request
|
||||
var url = parseURL(window.location.href);
|
||||
if (url.params.hasOwnProperty(URL_SEARCH_PARAM)
|
||||
&& url.params[URL_SEARCH_PARAM] != "") {
|
||||
showSearch(true);
|
||||
searchbar.value = decodeURIComponent(
|
||||
(url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20'));
|
||||
searchbarKeyUpHandler(); // -> doSearch()
|
||||
} else {
|
||||
showSearch(false);
|
||||
}
|
||||
|
||||
if (url.params.hasOwnProperty(URL_MARK_PARAM)) {
|
||||
var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
|
||||
marker.mark(words, {
|
||||
exclude: mark_exclude
|
||||
});
|
||||
|
||||
var markers = document.querySelectorAll("mark");
|
||||
function hide() {
|
||||
for (var i = 0; i < markers.length; i++) {
|
||||
markers[i].classList.add("fade-out");
|
||||
window.setTimeout(function(e) { marker.unmark(); }, 300);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < markers.length; i++) {
|
||||
markers[i].addEventListener('click', hide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for keyevents on `document`
|
||||
function globalKeyHandler(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)) { return; }
|
||||
|
||||
if (e.keyCode === ESCAPE_KEYCODE) {
|
||||
e.preventDefault();
|
||||
searchbar.classList.remove("active");
|
||||
setSearchUrlParameters("",
|
||||
(searchbar.value.trim() !== "") ? "push" : "replace");
|
||||
if (hasFocus()) {
|
||||
unfocusSearchbar();
|
||||
}
|
||||
showSearch(false);
|
||||
marker.unmark();
|
||||
} else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) {
|
||||
e.preventDefault();
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.select();
|
||||
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
|
||||
e.preventDefault();
|
||||
unfocusSearchbar();
|
||||
searchresults.firstElementChild.classList.add("focus");
|
||||
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|
||||
|| e.keyCode === UP_KEYCODE
|
||||
|| e.keyCode === SELECT_KEYCODE)) {
|
||||
// not `:focus` because browser does annoying scrolling
|
||||
var focused = searchresults.querySelector("li.focus");
|
||||
if (!focused) return;
|
||||
e.preventDefault();
|
||||
if (e.keyCode === DOWN_KEYCODE) {
|
||||
var next = focused.nextElementSibling;
|
||||
if (next) {
|
||||
focused.classList.remove("focus");
|
||||
next.classList.add("focus");
|
||||
}
|
||||
} else if (e.keyCode === UP_KEYCODE) {
|
||||
focused.classList.remove("focus");
|
||||
var prev = focused.previousElementSibling;
|
||||
if (prev) {
|
||||
prev.classList.add("focus");
|
||||
} else {
|
||||
searchbar.select();
|
||||
}
|
||||
} else { // SELECT_KEYCODE
|
||||
window.location.assign(focused.querySelector('a'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showSearch(yes) {
|
||||
if (yes) {
|
||||
search_wrap.classList.remove('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
search_wrap.classList.add('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'false');
|
||||
var results = searchresults.children;
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
results[i].classList.remove("focus");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showResults(yes) {
|
||||
if (yes) {
|
||||
searchresults_outer.classList.remove('hidden');
|
||||
} else {
|
||||
searchresults_outer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for search icon
|
||||
function searchIconClickHandler() {
|
||||
if (search_wrap.classList.contains('hidden')) {
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.select();
|
||||
} else {
|
||||
showSearch(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for keyevents while the searchbar is focused
|
||||
function searchbarKeyUpHandler() {
|
||||
var searchterm = searchbar.value.trim();
|
||||
if (searchterm != "") {
|
||||
searchbar.classList.add("active");
|
||||
doSearch(searchterm);
|
||||
} else {
|
||||
searchbar.classList.remove("active");
|
||||
showResults(false);
|
||||
removeChildren(searchresults);
|
||||
}
|
||||
|
||||
setSearchUrlParameters(searchterm, "push_if_new_search_else_replace");
|
||||
|
||||
// Remove marks
|
||||
marker.unmark();
|
||||
}
|
||||
|
||||
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor .
|
||||
// `action` can be one of "push", "replace", "push_if_new_search_else_replace"
|
||||
// and replaces or pushes a new browser history item.
|
||||
// "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
|
||||
function setSearchUrlParameters(searchterm, action) {
|
||||
var url = parseURL(window.location.href);
|
||||
var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM);
|
||||
if (searchterm != "" || action == "push_if_new_search_else_replace") {
|
||||
url.params[URL_SEARCH_PARAM] = searchterm;
|
||||
delete url.params[URL_MARK_PARAM];
|
||||
url.hash = "";
|
||||
} else {
|
||||
delete url.params[URL_MARK_PARAM];
|
||||
delete url.params[URL_SEARCH_PARAM];
|
||||
}
|
||||
// A new search will also add a new history item, so the user can go back
|
||||
// to the page prior to searching. A updated search term will only replace
|
||||
// the url.
|
||||
if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) {
|
||||
history.pushState({}, document.title, renderURL(url));
|
||||
} else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) {
|
||||
history.replaceState({}, document.title, renderURL(url));
|
||||
}
|
||||
}
|
||||
|
||||
function doSearch(searchterm) {
|
||||
|
||||
// Don't search the same twice
|
||||
if (current_searchterm == searchterm) { return; }
|
||||
else { current_searchterm = searchterm; }
|
||||
|
||||
if (searchindex == null) { return; }
|
||||
|
||||
// Do the actual search
|
||||
var results = searchindex.search(searchterm, search_options);
|
||||
var resultcount = Math.min(results.length, results_options.limit_results);
|
||||
|
||||
// Display search metrics
|
||||
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
|
||||
|
||||
// Clear and insert results
|
||||
var searchterms = searchterm.split(' ');
|
||||
removeChildren(searchresults);
|
||||
for(var i = 0; i < resultcount ; i++){
|
||||
var resultElem = document.createElement('li');
|
||||
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
|
||||
searchresults.appendChild(resultElem);
|
||||
}
|
||||
|
||||
// Display results
|
||||
showResults(true);
|
||||
}
|
||||
|
||||
fetch(path_to_root + 'searchindex.json')
|
||||
.then(response => response.json())
|
||||
.then(json => init(json))
|
||||
.catch(error => { // Try to load searchindex.js if fetch failed
|
||||
var script = document.createElement('script');
|
||||
script.src = path_to_root + 'searchindex.js';
|
||||
script.onload = () => init(window.search);
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
// Exported functions
|
||||
search.hasFocus = hasFocus;
|
||||
})(window.search);
|
||||
@@ -1,54 +0,0 @@
|
||||
// Populate the sidebar
|
||||
//
|
||||
// This is a script, and not included directly in the page, to control the total size of the book.
|
||||
// The TOC contains an entry for each page, so if each page includes a copy of the TOC,
|
||||
// the total size of the page becomes O(n**2).
|
||||
var sidebarScrollbox = document.querySelector("#sidebar .sidebar-scrollbox");
|
||||
sidebarScrollbox.innerHTML = '{{#toc}}{{/toc}}';
|
||||
(function() {
|
||||
let current_page = document.location.href.toString();
|
||||
if (current_page.endsWith("/")) {
|
||||
current_page += "index.html";
|
||||
}
|
||||
var links = sidebarScrollbox.querySelectorAll("a");
|
||||
var l = links.length;
|
||||
for (var i = 0; i < l; ++i) {
|
||||
var link = links[i];
|
||||
var href = link.getAttribute("href");
|
||||
if (href && !href.startsWith("#") && !/^(?:[a-z+]+:)?\/\//.test(href)) {
|
||||
link.href = path_to_root + href;
|
||||
}
|
||||
// The "index" page is supposed to alias the first chapter in the book.
|
||||
if (link.href === current_page || (i === 0 && path_to_root === "" && current_page.endsWith("/index.html"))) {
|
||||
link.classList.add("active");
|
||||
var parent = link.parentElement;
|
||||
while (parent) {
|
||||
if (parent.tagName === "LI" && parent.previousElementSibling) {
|
||||
if (parent.previousElementSibling.classList.contains("chapter-item")) {
|
||||
parent.previousElementSibling.classList.add("expanded");
|
||||
}
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Track and set sidebar scroll position
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Filesystem utilities and helpers.
|
||||
|
||||
use crate::errors::*;
|
||||
use log::{debug, trace};
|
||||
use std::fs::{self, File};
|
||||
@@ -202,6 +204,7 @@ 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()
|
||||
|
||||
190
src/utils/mod.rs
190
src/utils/mod.rs
@@ -1,11 +1,10 @@
|
||||
#![allow(missing_docs)] // FIXME: Document this
|
||||
//! Various helpers and utilities.
|
||||
|
||||
pub mod fs;
|
||||
mod string;
|
||||
pub(crate) mod toml_ext;
|
||||
use crate::errors::Error;
|
||||
use log::error;
|
||||
use once_cell::sync::Lazy;
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd};
|
||||
use regex::Regex;
|
||||
|
||||
@@ -13,6 +12,7 @@ use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub use self::string::{
|
||||
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
||||
@@ -21,7 +21,7 @@ pub use self::string::{
|
||||
|
||||
/// Replaces multiple consecutive whitespace characters with a single space character.
|
||||
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
|
||||
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s\s+").unwrap());
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s\s+").unwrap());
|
||||
RE.replace_all(text, " ")
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn id_from_content(content: &str) -> String {
|
||||
let mut content = content.to_string();
|
||||
|
||||
// Skip any tags or html-encoded stuff
|
||||
static HTML: Lazy<Regex> = Lazy::new(|| Regex::new(r"(<.*?>)").unwrap());
|
||||
static HTML: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(<.*?>)").unwrap());
|
||||
content = HTML.replace_all(&content, "").into();
|
||||
const REPL_SUB: &[&str] = &["<", ">", "&", "'", """];
|
||||
for sub in REPL_SUB {
|
||||
@@ -93,9 +93,10 @@ pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, us
|
||||
/// None. Ideally, print page links would link to anchors on the print page,
|
||||
/// but that is very difficult.
|
||||
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
static SCHEME_LINK: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
|
||||
static MD_LINK: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap());
|
||||
static SCHEME_LINK: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
|
||||
static MD_LINK: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap());
|
||||
|
||||
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
|
||||
if dest.starts_with('#') {
|
||||
@@ -148,8 +149,8 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
// There are dozens of HTML tags/attributes that contain paths, so
|
||||
// feel free to add more tags if desired; these are the only ones I
|
||||
// care about right now.
|
||||
static HTML_LINK: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap());
|
||||
static HTML_LINK: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap());
|
||||
|
||||
HTML_LINK
|
||||
.replace_all(&html, |caps: ®ex::Captures<'_>| {
|
||||
@@ -194,6 +195,7 @@ pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
|
||||
render_markdown_with_path(text, smart_punctuation, None)
|
||||
}
|
||||
|
||||
/// Creates a new pulldown-cmark parser of the given text.
|
||||
pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(Options::ENABLE_TABLES);
|
||||
@@ -207,23 +209,180 @@ pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
|
||||
Parser::new_ext(text, opts)
|
||||
}
|
||||
|
||||
/// Renders markdown to HTML.
|
||||
///
|
||||
/// `path` should only be set if this is being generated for the consolidated
|
||||
/// print page. It should point to the page being rendered relative to the
|
||||
/// root of the book.
|
||||
pub fn render_markdown_with_path(
|
||||
text: &str,
|
||||
smart_punctuation: bool,
|
||||
path: Option<&Path>,
|
||||
) -> String {
|
||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||
let p = new_cmark_parser(text, smart_punctuation);
|
||||
let events = p
|
||||
let mut body = String::with_capacity(text.len() * 3 / 2);
|
||||
|
||||
// Based on
|
||||
// https://github.com/pulldown-cmark/pulldown-cmark/blob/master/pulldown-cmark/examples/footnote-rewrite.rs
|
||||
|
||||
// This handling of footnotes is a two-pass process. This is done to
|
||||
// support linkbacks, little arrows that allow you to jump back to the
|
||||
// footnote reference. The first pass collects the footnote definitions.
|
||||
// The second pass modifies those definitions to include the linkbacks,
|
||||
// and inserts the definitions back into the `events` list.
|
||||
|
||||
// This is a map of name -> (number, count)
|
||||
// `name` is the name of the footnote.
|
||||
// `number` is the footnote number displayed in the output.
|
||||
// `count` is the number of references to this footnote (used for multiple
|
||||
// linkbacks, and checking for unused footnotes).
|
||||
let mut footnote_numbers = HashMap::new();
|
||||
// This is a map of name -> Vec<Event>
|
||||
// `name` is the name of the footnote.
|
||||
// The events list is the list of events needed to build the footnote definition.
|
||||
let mut footnote_defs = HashMap::new();
|
||||
|
||||
// The following are used when currently processing a footnote definition.
|
||||
//
|
||||
// This is the name of the footnote (escaped).
|
||||
let mut in_footnote_name = String::new();
|
||||
// This is the list of events to build the footnote definition.
|
||||
let mut in_footnote = Vec::new();
|
||||
|
||||
let events = new_cmark_parser(text, smart_punctuation)
|
||||
.map(clean_codeblock_headers)
|
||||
.map(|event| adjust_links(event, path))
|
||||
.flat_map(|event| {
|
||||
let (a, b) = wrap_tables(event);
|
||||
a.into_iter().chain(b)
|
||||
})
|
||||
// Footnote rewriting must go last to ensure inner definition contents
|
||||
// are processed (since they get pulled out of the initial stream).
|
||||
.filter_map(|event| {
|
||||
match event {
|
||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||
if !in_footnote.is_empty() {
|
||||
log::warn!("internal bug: nested footnote not expected in {path:?}");
|
||||
}
|
||||
in_footnote_name = special_escape(&name);
|
||||
None
|
||||
}
|
||||
Event::End(TagEnd::FootnoteDefinition) => {
|
||||
let def_events = std::mem::take(&mut in_footnote);
|
||||
let name = std::mem::take(&mut in_footnote_name);
|
||||
|
||||
if footnote_defs.contains_key(&name) {
|
||||
log::warn!(
|
||||
"footnote `{name}` in {} defined multiple times - \
|
||||
not updating to new definition",
|
||||
path.map_or_else(|| Cow::from("<unknown>"), |p| p.to_string_lossy())
|
||||
);
|
||||
} else {
|
||||
footnote_defs.insert(name, def_events);
|
||||
}
|
||||
None
|
||||
}
|
||||
Event::FootnoteReference(name) => {
|
||||
let name = special_escape(&name);
|
||||
let len = footnote_numbers.len() + 1;
|
||||
let (n, count) = footnote_numbers.entry(name.clone()).or_insert((len, 0));
|
||||
*count += 1;
|
||||
let html = Event::Html(
|
||||
format!(
|
||||
"<sup class=\"footnote-reference\" id=\"fr-{name}-{count}\">\
|
||||
<a href=\"#footnote-{name}\">{n}</a>\
|
||||
</sup>"
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
if in_footnote_name.is_empty() {
|
||||
Some(html)
|
||||
} else {
|
||||
// While inside a footnote, we need to accumulate.
|
||||
in_footnote.push(html);
|
||||
None
|
||||
}
|
||||
}
|
||||
// While inside a footnote, accumulate all events into a local.
|
||||
_ if !in_footnote_name.is_empty() => {
|
||||
in_footnote.push(event);
|
||||
None
|
||||
}
|
||||
_ => Some(event),
|
||||
}
|
||||
});
|
||||
|
||||
html::push_html(&mut s, events);
|
||||
s
|
||||
html::push_html(&mut body, events);
|
||||
|
||||
if !footnote_defs.is_empty() {
|
||||
add_footnote_defs(
|
||||
&mut body,
|
||||
path,
|
||||
footnote_defs.into_iter().collect(),
|
||||
&footnote_numbers,
|
||||
);
|
||||
}
|
||||
|
||||
body
|
||||
}
|
||||
|
||||
/// Adds all footnote definitions into `body`.
|
||||
fn add_footnote_defs(
|
||||
body: &mut String,
|
||||
path: Option<&Path>,
|
||||
mut defs: Vec<(String, Vec<Event<'_>>)>,
|
||||
numbers: &HashMap<String, (usize, u32)>,
|
||||
) {
|
||||
// Remove unused.
|
||||
defs.retain(|(name, _)| {
|
||||
if !numbers.contains_key(name) {
|
||||
log::warn!(
|
||||
"footnote `{name}` in `{}` is defined but not referenced",
|
||||
path.map_or_else(|| Cow::from("<unknown>"), |p| p.to_string_lossy())
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
defs.sort_by_cached_key(|(name, _)| numbers[name].0);
|
||||
|
||||
body.push_str(
|
||||
"<hr>\n\
|
||||
<ol class=\"footnote-definition\">",
|
||||
);
|
||||
|
||||
// Insert the backrefs to the definition, and put the definitions in the output.
|
||||
for (name, mut fn_events) in defs {
|
||||
let count = numbers[&name].1;
|
||||
fn_events.insert(
|
||||
0,
|
||||
Event::Html(format!("<li id=\"footnote-{name}\">").into()),
|
||||
);
|
||||
// Generate the linkbacks.
|
||||
for usage in 1..=count {
|
||||
let nth = if usage == 1 {
|
||||
String::new()
|
||||
} else {
|
||||
usage.to_string()
|
||||
};
|
||||
let backlink =
|
||||
Event::Html(format!(" <a href=\"#fr-{name}-{usage}\">↩{nth}</a>").into());
|
||||
if matches!(fn_events.last(), Some(Event::End(TagEnd::Paragraph))) {
|
||||
// Put the linkback at the end of the last paragraph instead
|
||||
// of on a line by itself.
|
||||
fn_events.insert(fn_events.len() - 1, backlink);
|
||||
} else {
|
||||
// Not a clear place to put it in this circumstance, so put it
|
||||
// at the end.
|
||||
fn_events.push(backlink);
|
||||
}
|
||||
}
|
||||
fn_events.push(Event::Html("</li>\n".into()));
|
||||
html::push_html(body, fn_events.into_iter());
|
||||
}
|
||||
|
||||
body.push_str("</ol>");
|
||||
}
|
||||
|
||||
/// Wraps tables in a `.table-wrapper` class to apply overflow-x rules to.
|
||||
@@ -267,13 +426,14 @@ pub fn log_backtrace(e: &Error) {
|
||||
|
||||
pub(crate) fn special_escape(mut s: &str) -> String {
|
||||
let mut escaped = String::with_capacity(s.len());
|
||||
let needs_escape: &[char] = &['<', '>', '\'', '\\', '&'];
|
||||
let needs_escape: &[char] = &['<', '>', '\'', '"', '\\', '&'];
|
||||
while let Some(next) = s.find(needs_escape) {
|
||||
escaped.push_str(&s[..next]);
|
||||
match s.as_bytes()[next] {
|
||||
b'<' => escaped.push_str("<"),
|
||||
b'>' => escaped.push_str(">"),
|
||||
b'\'' => escaped.push_str("'"),
|
||||
b'"' => escaped.push_str("""),
|
||||
b'\\' => escaped.push_str("\"),
|
||||
b'&' => escaped.push_str("&"),
|
||||
_ => unreachable!(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::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 {
|
||||
@@ -24,10 +24,10 @@ pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
static ANCHOR_START: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap());
|
||||
static ANCHOR_END: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap());
|
||||
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());
|
||||
|
||||
/// Take anchored lines from a string.
|
||||
/// Lines containing anchor are ignored.
|
||||
|
||||
@@ -9,6 +9,7 @@ edition = "2018"
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
hash-files = true
|
||||
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
@@ -24,4 +25,27 @@ expand = true
|
||||
heading-split-level = 2
|
||||
|
||||
[output.html.redirect]
|
||||
"/format/config.html" = "configuration/index.html"
|
||||
"/format/config.html" = "../prefix.html"
|
||||
|
||||
# This is a source without a fragment, and one with a fragment that goes to
|
||||
# the same place. The redirect with the fragment is not necessary, since that
|
||||
# is the default behavior.
|
||||
"/pointless-fragment.html" = "prefix.html"
|
||||
"/pointless-fragment.html#foo" = "prefix.html#foo"
|
||||
|
||||
"/rename-page-and-fragment.html" = "prefix.html"
|
||||
"/rename-page-and-fragment.html#orig" = "prefix.html#new"
|
||||
|
||||
"/rename-page-fragment-elsewhere.html" = "prefix.html"
|
||||
"/rename-page-fragment-elsewhere.html#orig" = "suffix.html#new"
|
||||
|
||||
# Rename fragment on an existing page.
|
||||
"/prefix.html#orig" = "prefix.html#new"
|
||||
# Rename fragment on an existing page to another page.
|
||||
"/prefix.html#orig-new-page" = "suffix.html#new"
|
||||
|
||||
"/full-url-with-fragment.html" = "https://www.rust-lang.org/#fragment"
|
||||
|
||||
"/full-url-with-fragment-map.html" = "https://www.rust-lang.org/"
|
||||
"/full-url-with-fragment-map.html#a" = "https://www.rust-lang.org/#new1"
|
||||
"/full-url-with-fragment-map.html#b" = "https://www.rust-lang.org/#new2"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user