mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 15:01:45 -05:00
Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a7ca2e169f | ||
|
|
32daca669a | ||
|
|
cf5a78c0e1 | ||
|
|
0fb814c6d6 |
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
|
||||
|
||||
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -48,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: |
|
||||
|
||||
35
.github/workflows/main.yml
vendored
35
.github/workflows/main.yml
vendored
@@ -3,9 +3,6 @@ on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
env:
|
||||
BROWSER_UI_TEST_VERSION: '0.19.0'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -43,7 +40,7 @@ jobs:
|
||||
- name: msrv
|
||||
os: ubuntu-22.04
|
||||
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
|
||||
rust: 1.77.0
|
||||
rust: 1.82.0
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: ${{ matrix.name }}
|
||||
steps:
|
||||
@@ -85,10 +82,34 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install browser-ui-test
|
||||
run: npm install browser-ui-test@"${BROWSER_UI_TEST_VERSION}"
|
||||
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
|
||||
@@ -100,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) }}'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,6 +9,7 @@ guide/book
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
test_book/book/
|
||||
tests/testsuite/*/*/book/
|
||||
|
||||
# Ignore Jetbrains specific files.
|
||||
.idea/
|
||||
|
||||
78
CHANGELOG.md
78
CHANGELOG.md
@@ -1,5 +1,83 @@
|
||||
# Changelog
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -144,9 +143,16 @@ GUI tests are checked with the GUI testsuite. To run it, you need to install `np
|
||||
cargo test --test gui
|
||||
```
|
||||
|
||||
The first time, it'll fail and ask you to install the `browser-ui-test` package. Install it then re-run the tests.
|
||||
If you want to only run some tests, you can filter them by passing (part of) their name:
|
||||
|
||||
If you want to disable the headless mode, use the `DISABLE_HEADLESS_TEST=1` environment variable:
|
||||
```
|
||||
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
|
||||
@@ -156,6 +162,21 @@ The GUI tests are in the directory `tests/gui` in text files with the `.goml` ex
|
||||
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
|
||||
|
||||
The following are instructions for updating [highlight.js](https://highlightjs.org/).
|
||||
|
||||
956
Cargo.lock
generated
956
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
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.47"
|
||||
version = "0.4.50"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
@@ -17,20 +23,19 @@ license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
description = "Creates a book from markdown files"
|
||||
rust-version = "1.77" # Keep in sync with installation.md and .github/workflows/main.yml
|
||||
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"] }
|
||||
@@ -50,7 +55,7 @@ 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 }
|
||||
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.6", default-features = false, features = ["websocket"], optional = true }
|
||||
|
||||
# Search feature
|
||||
@@ -58,10 +63,9 @@ 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"
|
||||
|
||||
@@ -91,3 +95,6 @@ test = false
|
||||
name = "gui"
|
||||
path = "tests/gui/runner.rs"
|
||||
crate-type = ["bin"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -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."));
|
||||
|
||||
@@ -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.47/mdbook-v0.4.47-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.50/mdbook-v0.4.50-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
|
||||
|
||||
@@ -122,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
|
||||
|
||||
@@ -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.77.
|
||||
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.19.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -647,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;
|
||||
@@ -860,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();
|
||||
@@ -1127,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""#);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -474,9 +474,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 {
|
||||
@@ -641,3 +656,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;
|
||||
}
|
||||
@@ -200,18 +200,53 @@ sup {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
:not(.footnote-definition) + .footnote-definition {
|
||||
margin-block-start: 2em;
|
||||
}
|
||||
.footnote-definition:not(:has(+ .footnote-definition)) {
|
||||
margin-block-end: 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,7 +2,8 @@
|
||||
/* 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;
|
||||
@@ -61,6 +62,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 +115,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 +168,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 +221,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 +272,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 |
|
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 |
818
src/front-end/js/book.js
Normal file
818
src/front-end/js/book.js
Normal file
@@ -0,0 +1,818 @@
|
||||
'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 body = document.querySelector('body');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
const sidebarToggleButton = document.getElementById('sidebar-toggle');
|
||||
const sidebarToggleAnchor = document.getElementById('sidebar-toggle-anchor');
|
||||
const sidebarResizeHandle = document.getElementById('sidebar-resize-handle');
|
||||
let 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) {
|
||||
// Ignore error.
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// Ignore error.
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
sidebarToggleAnchor.addEventListener('change', function sidebarToggle() {
|
||||
if (sidebarToggleAnchor.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);
|
||||
body.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
let 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-target-width', pos + 'px');
|
||||
}
|
||||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize() {
|
||||
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;
|
||||
}
|
||||
|
||||
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 reforesh 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
|
||||
@@ -1,6 +1,9 @@
|
||||
"use strict";
|
||||
'use strict';
|
||||
|
||||
/* global Mark, elasticlunr, path_to_root */
|
||||
|
||||
window.search = window.search || {};
|
||||
(function search(search) {
|
||||
(function search() {
|
||||
// Search functionality
|
||||
//
|
||||
// You can use !hasFocus() to prevent keyhandling in your key
|
||||
@@ -10,49 +13,45 @@ window.search = window.search || {};
|
||||
return;
|
||||
}
|
||||
|
||||
//IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
||||
// 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;
|
||||
};
|
||||
}
|
||||
|
||||
var search_wrap = document.getElementById('search-wrapper'),
|
||||
const 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,
|
||||
// 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 = [],
|
||||
results_options = {
|
||||
teaser_word_count: 30,
|
||||
limit_results: 30,
|
||||
},
|
||||
search_options = {
|
||||
bool: "AND",
|
||||
bool: 'AND',
|
||||
expand: true,
|
||||
fields: {
|
||||
title: {boost: 1},
|
||||
body: {boost: 1},
|
||||
breadcrumbs: {boost: 0}
|
||||
}
|
||||
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;
|
||||
searchindex = null,
|
||||
results_options = {
|
||||
teaser_word_count: 30,
|
||||
limit_results: 30,
|
||||
},
|
||||
teaser_count = 0;
|
||||
|
||||
function hasFocus() {
|
||||
return searchbar === document.activeElement;
|
||||
@@ -66,96 +65,99 @@ window.search = window.search || {};
|
||||
|
||||
// Helper to parse a url into its building blocks.
|
||||
function parseURL(url) {
|
||||
var a = document.createElement('a');
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
return {
|
||||
source: url,
|
||||
protocol: a.protocol.replace(':',''),
|
||||
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('=');
|
||||
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')
|
||||
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;
|
||||
let 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 = "&";
|
||||
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;
|
||||
if (urlobject.hash !== '') {
|
||||
url += '#' + urlobject.hash;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
// Helper to escape html special chars for displaying the teasers
|
||||
var escapeHTML = (function() {
|
||||
var MAP = {
|
||||
const escapeHTML = (function() {
|
||||
const MAP = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
'\'': ''',
|
||||
};
|
||||
const repl = function(c) {
|
||||
return MAP[c];
|
||||
};
|
||||
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 + "'.";
|
||||
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 + "':";
|
||||
return count + ' search results for \'' + searchterm + '\':';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function formatSearchResult(result, searchterms) {
|
||||
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
|
||||
const 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("");
|
||||
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).
|
||||
var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27");
|
||||
const encoded_search = 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>';
|
||||
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:
|
||||
@@ -166,88 +168,90 @@ window.search = window.search || {};
|
||||
// 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) {
|
||||
const 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]
|
||||
const searchterm_weight = 40;
|
||||
const 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(' ');
|
||||
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 (var wordindex in words) {
|
||||
var word = words[wordindex];
|
||||
for (const wordindex in words) {
|
||||
const word = words[wordindex];
|
||||
if (word.length > 0) {
|
||||
for (var searchtermindex in stemmed_searchterms) {
|
||||
if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) {
|
||||
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) {
|
||||
if (weighted.length === 0) {
|
||||
return body;
|
||||
}
|
||||
|
||||
var window_weight = [];
|
||||
var window_size = Math.min(weighted.length, results_options.teaser_word_count);
|
||||
const window_weight = [];
|
||||
const window_size = Math.min(weighted.length, results_options.teaser_word_count);
|
||||
|
||||
var cur_sum = 0;
|
||||
for (var wordindex = 0; wordindex < window_size; wordindex++) {
|
||||
let cur_sum = 0;
|
||||
for (let 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++) {
|
||||
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) {
|
||||
var max_sum = 0;
|
||||
var max_sum_window_index = 0;
|
||||
let max_sum = 0;
|
||||
// backwards
|
||||
for (var i = window_weight.length - 1; i >= 0; i--) {
|
||||
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
|
||||
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];
|
||||
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>")
|
||||
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>")
|
||||
if (word[1] === searchterm_weight) {
|
||||
teaser_split.push('</em>');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return teaser_split.join('');
|
||||
}
|
||||
@@ -255,115 +259,148 @@ window.search = window.search || {};
|
||||
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);
|
||||
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 = function(e) { doSearchOrMarkFromUrl(); };
|
||||
window.onpopstate = () => {
|
||||
doSearchOrMarkFromUrl();
|
||||
};
|
||||
// Suppress "submit" events so the page doesn't reload when the user presses Enter
|
||||
document.addEventListener('submit', function(e) { e.preventDefault(); }, false);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
function unfocusSearchbar() {
|
||||
// hacky, but just focusing a div only works once
|
||||
var tmp = document.createElement('input');
|
||||
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
|
||||
var url = parseURL(window.location.href);
|
||||
if (url.params.hasOwnProperty(URL_SEARCH_PARAM)
|
||||
&& url.params[URL_SEARCH_PARAM] != "") {
|
||||
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'));
|
||||
(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(' ');
|
||||
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
|
||||
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);
|
||||
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 (var i = 0; i < markers.length; i++) {
|
||||
};
|
||||
|
||||
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.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) {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
searchbar.classList.remove("active");
|
||||
setSearchUrlParameters("",
|
||||
(searchbar.value.trim() !== "") ? "push" : "replace");
|
||||
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) {
|
||||
} else if (!hasFocus() && (e.key === 'S' || e.key === '/')) {
|
||||
e.preventDefault();
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.select();
|
||||
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
|
||||
} else if (hasFocus() && (e.key === 'ArrowDown'
|
||||
|| e.key === 'Enter')) {
|
||||
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");
|
||||
const first = searchresults.firstElementChild;
|
||||
if (first !== null) {
|
||||
unfocusSearchbar();
|
||||
first.classList.add('focus');
|
||||
if (e.key === 'Enter') {
|
||||
window.location.assign(first.querySelector('a'));
|
||||
}
|
||||
} else if (e.keyCode === UP_KEYCODE) {
|
||||
focused.classList.remove("focus");
|
||||
var prev = focused.previousElementSibling;
|
||||
}
|
||||
} 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");
|
||||
prev.classList.add('focus');
|
||||
} else {
|
||||
searchbar.select();
|
||||
}
|
||||
} else { // SELECT_KEYCODE
|
||||
} else { // Enter
|
||||
window.location.assign(focused.querySelector('a'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showSearch(yes) {
|
||||
if (yes) {
|
||||
search_wrap.classList.remove('hidden');
|
||||
@@ -371,9 +408,9 @@ window.search = window.search || {};
|
||||
} 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");
|
||||
const results = searchresults.children;
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
results[i].classList.remove('focus');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -396,36 +433,37 @@ window.search = window.search || {};
|
||||
showSearch(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Eventhandler for keyevents while the searchbar is focused
|
||||
function searchbarKeyUpHandler() {
|
||||
var searchterm = searchbar.value.trim();
|
||||
if (searchterm != "") {
|
||||
searchbar.classList.add("active");
|
||||
const searchterm = searchbar.value.trim();
|
||||
if (searchterm !== '') {
|
||||
searchbar.classList.add('active');
|
||||
doSearch(searchterm);
|
||||
} else {
|
||||
searchbar.classList.remove("active");
|
||||
searchbar.classList.remove('active');
|
||||
showResults(false);
|
||||
removeChildren(searchresults);
|
||||
}
|
||||
|
||||
setSearchUrlParameters(searchterm, "push_if_new_search_else_replace");
|
||||
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.
|
||||
|
||||
// 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") {
|
||||
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 = "";
|
||||
url.hash = '';
|
||||
} else {
|
||||
delete url.params[URL_MARK_PARAM];
|
||||
delete url.params[URL_SEARCH_PARAM];
|
||||
@@ -433,33 +471,40 @@ window.search = window.search || {};
|
||||
// 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) ) {
|
||||
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) ) {
|
||||
} 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 (current_searchterm === searchterm) {
|
||||
return;
|
||||
} else {
|
||||
current_searchterm = searchterm;
|
||||
}
|
||||
|
||||
if (searchindex == null) { return; }
|
||||
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);
|
||||
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
|
||||
var searchterms = searchterm.split(' ');
|
||||
const searchterms = searchterm.split(' ');
|
||||
removeChildren(searchresults);
|
||||
for(var i = 0; i < resultcount ; i++){
|
||||
var resultElem = document.createElement('li');
|
||||
for (let i = 0; i < resultcount ; i++) {
|
||||
const resultElem = document.createElement('li');
|
||||
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
|
||||
searchresults.appendChild(resultElem);
|
||||
}
|
||||
@@ -468,16 +513,17 @@ window.search = window.search || {};
|
||||
showResults(true);
|
||||
}
|
||||
|
||||
fetch(path_to_root + '{{ resource "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 + '{{ resource "searchindex.js" }}';
|
||||
script.onload = () => init(window.search);
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
function loadScript(url, id) {
|
||||
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);
|
||||
}
|
||||
|
||||
loadScript(path_to_root + '{{ resource "searchindex.js" }}', 'search-index');
|
||||
|
||||
// Exported functions
|
||||
search.hasFocus = hasFocus;
|
||||
})(window.search);
|
||||
@@ -53,21 +53,35 @@
|
||||
<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 to javascript -->
|
||||
<!-- Provide site root and default themes 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 }}";
|
||||
const path_to_root = "{{ path_to_root }}";
|
||||
const default_light_theme = "{{ default_theme }}";
|
||||
const default_dark_theme = "{{ preferred_dark_theme }}";
|
||||
</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">
|
||||
<!-- 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));
|
||||
@@ -81,7 +95,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;
|
||||
@@ -94,8 +109,8 @@
|
||||
|
||||
<!-- 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';
|
||||
@@ -132,6 +147,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 +155,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}}
|
||||
@@ -251,7 +267,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...
|
||||
@@ -281,8 +297,8 @@
|
||||
|
||||
{{#if playground_js}}
|
||||
<script src="{{ resource "ace.js" }}"></script>
|
||||
<script src="{{ resource "editor.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}}
|
||||
@@ -10,7 +10,7 @@ class MDBookSidebarScrollbox extends HTMLElement {
|
||||
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];
|
||||
let current_page = document.location.href.toString().split("#")[0].split("?")[0];
|
||||
if (current_page.endsWith("/")) {
|
||||
current_page += "index.html";
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -12,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
|
||||
}
|
||||
@@ -207,7 +209,6 @@ impl HtmlHandlebars {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
fn post_process(
|
||||
&self,
|
||||
rendered: String,
|
||||
@@ -663,10 +664,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();
|
||||
|
||||
@@ -696,7 +697,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,
|
||||
@@ -724,8 +725,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<'_>| {
|
||||
@@ -738,8 +739,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,
|
||||
@@ -807,8 +808,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<'_>| {
|
||||
@@ -849,7 +852,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();
|
||||
|
||||
@@ -39,12 +39,7 @@ impl HelperDef for ResourceHelper {
|
||||
let path_to_root = utils::fs::path_to_root(&base_path);
|
||||
|
||||
out.write(&path_to_root)?;
|
||||
out.write(
|
||||
self.hash_map
|
||||
.get(¶m[..])
|
||||
.map(|p| &p[..])
|
||||
.unwrap_or(¶m),
|
||||
)?;
|
||||
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,5 +1,3 @@
|
||||
#![allow(missing_docs)] // FIXME: Document this
|
||||
|
||||
pub use self::hbs_renderer::HtmlHandlebars;
|
||||
pub use self::static_files::StaticFiles;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
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, Chapter};
|
||||
@@ -60,14 +60,19 @@ pub fn create_files(
|
||||
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 {
|
||||
static_files.add_builtin("searchindex.json", index.as_bytes());
|
||||
static_files.add_builtin(
|
||||
"searchindex.js",
|
||||
format!("Object.assign(window.search, {});", index).as_bytes(),
|
||||
// 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 = 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);
|
||||
@@ -309,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");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Support for writing static files.
|
||||
|
||||
use log::{debug, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::config::HtmlConfig;
|
||||
use crate::errors::*;
|
||||
@@ -13,6 +12,7 @@ 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.
|
||||
///
|
||||
@@ -231,8 +231,8 @@ impl StaticFiles {
|
||||
use regex::bytes::{Captures, Regex};
|
||||
// The `{{ resource "name" }}` directive in static resources look like
|
||||
// handlebars syntax, even if they technically aren't.
|
||||
static RESOURCE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"\{\{ resource "([^"]+)" \}\}"#).unwrap());
|
||||
static RESOURCE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r#"\{\{ resource "([^"]+)" \}\}"#).unwrap());
|
||||
fn replace_all<'a>(
|
||||
hash_map: &HashMap<String, String>,
|
||||
data: &'a [u8],
|
||||
|
||||
@@ -1,685 +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";
|
||||
classes.forEach(className => {
|
||||
if (className.startsWith("edition")) {
|
||||
edition = className.slice(7);
|
||||
}
|
||||
});
|
||||
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("#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');
|
||||
});
|
||||
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 sidebarToggleAnchor = document.getElementById("sidebar-toggle-anchor");
|
||||
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) { }
|
||||
}
|
||||
|
||||
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
|
||||
sidebarToggleAnchor.addEventListener('change', function sidebarToggle() {
|
||||
if (sidebarToggleAnchor.checked) {
|
||||
var current_width = parseInt(
|
||||
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
|
||||
if (current_width < 150) {
|
||||
document.documentElement.style.setProperty('--sidebar-width', '150px');
|
||||
}
|
||||
showSidebar();
|
||||
} else {
|
||||
hideSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
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,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.
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
//! Integration tests to make sure alternative backends work.
|
||||
|
||||
use mdbook::config::Config;
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
#[test]
|
||||
fn passing_alternate_backend() {
|
||||
let (md, _temp) = dummy_book_with_backend("passing", success_cmd(), false);
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failing_alternate_backend() {
|
||||
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd(), false);
|
||||
|
||||
md.build().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_backends_are_fatal() {
|
||||
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", false);
|
||||
let got = md.build();
|
||||
assert!(got.is_err());
|
||||
let error_message = got.err().unwrap().to_string();
|
||||
assert_eq!(error_message, "Rendering failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_optional_backends_are_not_fatal() {
|
||||
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", true);
|
||||
assert!(md.build().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternate_backend_with_arguments() {
|
||||
let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!", false);
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backends_receive_render_context_via_stdin() {
|
||||
use mdbook::renderer::RenderContext;
|
||||
use std::fs::File;
|
||||
|
||||
let (md, temp) = dummy_book_with_backend("cat-to-file", "renderers/myrenderer", false);
|
||||
|
||||
let renderers = temp.path().join("renderers");
|
||||
fs::create_dir(&renderers).unwrap();
|
||||
rust_exe(
|
||||
&renderers,
|
||||
"myrenderer",
|
||||
r#"fn main() {
|
||||
use std::io::Read;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_to_string(&mut s).unwrap();
|
||||
std::fs::write("out.txt", s).unwrap();
|
||||
}"#,
|
||||
);
|
||||
|
||||
let out_file = temp.path().join("book/out.txt");
|
||||
|
||||
assert!(!out_file.exists());
|
||||
md.build().unwrap();
|
||||
assert!(out_file.exists());
|
||||
|
||||
let got = RenderContext::from_json(File::open(&out_file).unwrap());
|
||||
assert!(got.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_command_path() {
|
||||
// Checks behavior of relative paths for the `command` setting.
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
let renderers = temp.path().join("renderers");
|
||||
fs::create_dir(&renderers).unwrap();
|
||||
rust_exe(
|
||||
&renderers,
|
||||
"myrenderer",
|
||||
r#"fn main() {
|
||||
std::fs::write("output", "test").unwrap();
|
||||
}"#,
|
||||
);
|
||||
let do_test = |cmd_path| {
|
||||
let mut config = Config::default();
|
||||
config
|
||||
.set("output.html", toml::value::Table::new())
|
||||
.unwrap();
|
||||
config.set("output.myrenderer.command", cmd_path).unwrap();
|
||||
let md = MDBook::init(temp.path())
|
||||
.with_config(config)
|
||||
.build()
|
||||
.unwrap();
|
||||
let output = temp.path().join("book/myrenderer/output");
|
||||
assert!(!output.exists());
|
||||
md.build().unwrap();
|
||||
assert!(output.exists());
|
||||
fs::remove_file(output).unwrap();
|
||||
};
|
||||
// Legacy paths work, relative to the output directory.
|
||||
if cfg!(windows) {
|
||||
do_test("../../renderers/myrenderer.exe");
|
||||
} else {
|
||||
do_test("../../renderers/myrenderer");
|
||||
}
|
||||
// Modern path, relative to the book directory.
|
||||
do_test("renderers/myrenderer");
|
||||
}
|
||||
|
||||
fn dummy_book_with_backend(
|
||||
name: &str,
|
||||
command: &str,
|
||||
backend_is_optional: bool,
|
||||
) -> (MDBook, TempDir) {
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
|
||||
let mut config = Config::default();
|
||||
config
|
||||
.set(format!("output.{name}.command"), command)
|
||||
.unwrap();
|
||||
|
||||
if backend_is_optional {
|
||||
config.set(format!("output.{name}.optional"), true).unwrap();
|
||||
}
|
||||
|
||||
let md = MDBook::init(temp.path())
|
||||
.with_config(config)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
(md, temp)
|
||||
}
|
||||
|
||||
fn fail_cmd() -> &'static str {
|
||||
if cfg!(windows) {
|
||||
r#"cmd.exe /c "exit 1""#
|
||||
} else {
|
||||
"false"
|
||||
}
|
||||
}
|
||||
|
||||
fn success_cmd() -> &'static str {
|
||||
if cfg!(windows) {
|
||||
r#"cmd.exe /c "exit 0""#
|
||||
} else {
|
||||
"true"
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_exe(temp: &Path, name: &str, src: &str) {
|
||||
let rs = temp.join(name).with_extension("rs");
|
||||
fs::write(&rs, src).unwrap();
|
||||
let status = std::process::Command::new("rustc")
|
||||
.arg(rs)
|
||||
.current_dir(temp)
|
||||
.status()
|
||||
.expect("rustc should run");
|
||||
assert!(status.success());
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
mod dummy_book;
|
||||
|
||||
use crate::dummy_book::DummyBook;
|
||||
use mdbook::book::Book;
|
||||
use mdbook::config::Config;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use mdbook::renderer::{RenderContext, Renderer};
|
||||
use mdbook::MDBook;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
struct Spy(Arc<Mutex<Inner>>);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Inner {
|
||||
run_count: usize,
|
||||
rendered_with: Vec<String>,
|
||||
}
|
||||
|
||||
impl Preprocessor for Spy {
|
||||
fn name(&self) -> &str {
|
||||
"dummy"
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
|
||||
let mut inner = self.0.lock().unwrap();
|
||||
inner.run_count += 1;
|
||||
inner.rendered_with.push(ctx.renderer.clone());
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for Spy {
|
||||
fn name(&self) -> &str {
|
||||
"dummy"
|
||||
}
|
||||
|
||||
fn render(&self, _ctx: &RenderContext) -> Result<()> {
|
||||
let mut inner = self.0.lock().unwrap();
|
||||
inner.run_count += 1;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_runs_preprocessors() {
|
||||
let spy: Arc<Mutex<Inner>> = Default::default();
|
||||
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let cfg = Config::default();
|
||||
|
||||
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
|
||||
book.with_preprocessor(Spy(Arc::clone(&spy)));
|
||||
book.build().unwrap();
|
||||
|
||||
let inner = spy.lock().unwrap();
|
||||
assert_eq!(inner.run_count, 1);
|
||||
assert_eq!(inner.rendered_with.len(), 1);
|
||||
assert_eq!(
|
||||
"html", inner.rendered_with[0],
|
||||
"We should have been run with the default HTML renderer"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_runs_renderers() {
|
||||
let spy: Arc<Mutex<Inner>> = Default::default();
|
||||
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let cfg = Config::default();
|
||||
|
||||
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
|
||||
book.with_renderer(Spy(Arc::clone(&spy)));
|
||||
book.build().unwrap();
|
||||
|
||||
let inner = spy.lock().unwrap();
|
||||
assert_eq!(inner.run_count, 1);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use crate::cli::cmd::mdbook_cmd;
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
||||
#[test]
|
||||
fn mdbook_cli_dummy_book_generates_index_html() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
|
||||
// doesn't exist before
|
||||
assert!(!temp.path().join("book").exists());
|
||||
|
||||
let mut cmd = mdbook_cmd();
|
||||
cmd.arg("build").current_dir(temp.path());
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stderr(
|
||||
predicates::str::is_match(r##"Stack depth exceeded in first[\\/]recursive.md."##)
|
||||
.unwrap(),
|
||||
)
|
||||
.stderr(predicates::str::contains(
|
||||
r##"[INFO] (mdbook::book): Running the html backend"##,
|
||||
));
|
||||
|
||||
// exists afterward
|
||||
assert!(temp.path().join("book").exists());
|
||||
|
||||
let index_file = temp.path().join("book/index.html");
|
||||
assert!(index_file.exists());
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
use assert_cmd::Command;
|
||||
|
||||
pub(crate) fn mdbook_cmd() -> Command {
|
||||
let mut cmd = Command::cargo_bin("mdbook").unwrap();
|
||||
cmd.env_remove("RUST_LOG");
|
||||
cmd
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
use crate::cli::cmd::mdbook_cmd;
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
||||
use mdbook::config::Config;
|
||||
|
||||
/// Run `mdbook init` with `--force` to skip the confirmation prompts
|
||||
#[test]
|
||||
fn base_mdbook_init_can_skip_confirmation_prompts() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
|
||||
// doesn't exist before
|
||||
assert!(!temp.path().join("book").exists());
|
||||
|
||||
let mut cmd = mdbook_cmd();
|
||||
cmd.args(["init", "--force"]).current_dir(temp.path());
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(predicates::str::contains("\nAll done, no errors...\n"));
|
||||
|
||||
let config = Config::from_disk(temp.path().join("book.toml")).unwrap();
|
||||
assert_eq!(config.book.title, None);
|
||||
|
||||
assert!(!temp.path().join(".gitignore").exists());
|
||||
}
|
||||
|
||||
/// Run `mdbook init` with `--title` without git config.
|
||||
///
|
||||
/// Regression test for https://github.com/rust-lang/mdBook/issues/2485
|
||||
#[test]
|
||||
fn no_git_config_with_title() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
|
||||
// doesn't exist before
|
||||
assert!(!temp.path().join("book").exists());
|
||||
|
||||
let mut cmd = mdbook_cmd();
|
||||
cmd.args(["init", "--title", "Example title"])
|
||||
.current_dir(temp.path())
|
||||
.env("GIT_CONFIG_GLOBAL", "")
|
||||
.env("GIT_CONFIG_NOSYSTEM", "1");
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(predicates::str::contains("\nAll done, no errors...\n"));
|
||||
|
||||
let config = Config::from_disk(temp.path().join("book.toml")).unwrap();
|
||||
assert_eq!(config.book.title.as_deref(), Some("Example title"));
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
mod build;
|
||||
mod cmd;
|
||||
mod init;
|
||||
mod test;
|
||||
@@ -1,34 +0,0 @@
|
||||
use crate::cli::cmd::mdbook_cmd;
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
||||
use predicates::boolean::PredicateBooleanExt;
|
||||
|
||||
#[test]
|
||||
fn mdbook_cli_can_correctly_test_a_passing_book() {
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
|
||||
let mut cmd = mdbook_cmd();
|
||||
cmd.arg("test").current_dir(temp.path());
|
||||
cmd.assert().success()
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "README.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "intro.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]index.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]nested.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"returned an error:\n\n"##).unwrap().not())
|
||||
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap().not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_cli_detects_book_with_failing_tests() {
|
||||
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
|
||||
|
||||
let mut cmd = mdbook_cmd();
|
||||
cmd.arg("test").current_dir(temp.path());
|
||||
cmd.assert().failure()
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "README.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "intro.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]index.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]nested.md""##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"returned an error:\n\n"##).unwrap())
|
||||
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap());
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
mod cli;
|
||||
mod dummy_book;
|
||||
@@ -1,56 +0,0 @@
|
||||
mod dummy_book;
|
||||
|
||||
use crate::dummy_book::DummyBook;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
|
||||
use mdbook::MDBook;
|
||||
|
||||
fn example() -> CmdPreprocessor {
|
||||
CmdPreprocessor::new(
|
||||
"nop-preprocessor".to_string(),
|
||||
"cargo run --example nop-preprocessor --".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn example_supports_whatever() {
|
||||
let cmd = example();
|
||||
|
||||
let got = cmd.supports_renderer("whatever");
|
||||
|
||||
assert_eq!(got, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn example_doesnt_support_not_supported() {
|
||||
let cmd = example();
|
||||
|
||||
let got = cmd.supports_renderer("not-supported");
|
||||
|
||||
assert_eq!(got, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ask_the_preprocessor_to_blow_up() {
|
||||
let dummy_book = DummyBook::new();
|
||||
let temp = dummy_book.build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
md.with_preprocessor(example());
|
||||
|
||||
md.config
|
||||
.set("preprocessor.nop-preprocessor.blow-up", true)
|
||||
.unwrap();
|
||||
|
||||
let got = md.build();
|
||||
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_the_dummy_book() {
|
||||
let dummy_book = DummyBook::new();
|
||||
let temp = dummy_book.build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
md.with_preprocessor(example());
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
//! This will create an entire book in a temporary directory using some
|
||||
//! dummy contents from the `tests/dummy-book/` directory.
|
||||
|
||||
// Not all features are used in all test crates, so...
|
||||
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
|
||||
|
||||
use anyhow::Context;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::MDBook;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Create a dummy book in a temporary directory, using the contents of
|
||||
/// `SUMMARY_MD` as a guide.
|
||||
///
|
||||
/// The "Nested Chapter" file contains a code block with a single
|
||||
/// `assert!($TEST_STATUS)`. If you want to check MDBook's testing
|
||||
/// functionality, `$TEST_STATUS` can be substitute for either `true` or
|
||||
/// `false`. This is done using the `passing_test` parameter.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DummyBook {
|
||||
passing_test: bool,
|
||||
}
|
||||
|
||||
impl DummyBook {
|
||||
/// Create a new `DummyBook` with all the defaults.
|
||||
pub fn new() -> DummyBook {
|
||||
DummyBook { passing_test: true }
|
||||
}
|
||||
|
||||
/// Whether the doc-test included in the "Nested Chapter" should pass or
|
||||
/// fail (it passes by default).
|
||||
pub fn with_passing_test(&mut self, test_passes: bool) -> &mut DummyBook {
|
||||
self.passing_test = test_passes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Write a book to a temporary directory using the provided settings.
|
||||
pub fn build(&self) -> Result<TempDir> {
|
||||
let temp = TempFileBuilder::new()
|
||||
.prefix("dummy_book-")
|
||||
.tempdir()
|
||||
.with_context(|| "Unable to create temp directory")?;
|
||||
|
||||
let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
|
||||
recursive_copy(&dummy_book_root, temp.path()).with_context(|| {
|
||||
"Couldn't copy files into a \
|
||||
temporary directory"
|
||||
})?;
|
||||
|
||||
let sub_pattern = if self.passing_test { "true" } else { "false" };
|
||||
let files_containing_tests = [
|
||||
"src/first/nested.md",
|
||||
"src/first/nested-test.rs",
|
||||
"src/first/nested-test-with-anchors.rs",
|
||||
"src/first/partially-included-test.rs",
|
||||
"src/first/partially-included-test-with-anchors.rs",
|
||||
];
|
||||
for file in &files_containing_tests {
|
||||
let path_containing_tests = temp.path().join(file);
|
||||
replace_pattern_in_file(&path_containing_tests, "$TEST_STATUS", sub_pattern)?;
|
||||
}
|
||||
|
||||
Ok(temp)
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()> {
|
||||
let contents = fs::read_to_string(filename)?;
|
||||
File::create(filename)?.write_all(contents.replace(from, to).as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read the contents of the provided file into memory and then iterate through
|
||||
/// the list of strings asserting that the file contains all of them.
|
||||
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
|
||||
let filename = filename.as_ref();
|
||||
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
|
||||
|
||||
for s in strings {
|
||||
assert!(
|
||||
content.contains(s),
|
||||
"Searching for {:?} in {}\n\n{}",
|
||||
s,
|
||||
filename.display(),
|
||||
content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_doesnt_contain_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
|
||||
let filename = filename.as_ref();
|
||||
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
|
||||
|
||||
for s in strings {
|
||||
assert!(
|
||||
!content.contains(s),
|
||||
"Found {:?} in {}\n\n{}",
|
||||
s,
|
||||
filename.display(),
|
||||
content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively copy an entire directory tree to somewhere else (a la `cp -r`).
|
||||
fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()> {
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
|
||||
for entry in WalkDir::new(from) {
|
||||
let entry = entry.with_context(|| "Unable to inspect directory entry")?;
|
||||
|
||||
let original_location = entry.path();
|
||||
let relative = original_location
|
||||
.strip_prefix(from)
|
||||
.expect("`original_location` is inside the `from` directory");
|
||||
let new_location = to.join(relative);
|
||||
|
||||
if original_location.is_file() {
|
||||
if let Some(parent) = new_location.parent() {
|
||||
fs::create_dir_all(parent).with_context(|| "Couldn't create directory")?;
|
||||
}
|
||||
|
||||
fs::copy(original_location, &new_location)
|
||||
.with_context(|| "Unable to copy file contents")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new_copy_of_example_book() -> Result<TempDir> {
|
||||
let temp = TempFileBuilder::new().prefix("guide").tempdir()?;
|
||||
|
||||
let guide = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
|
||||
|
||||
recursive_copy(guide, temp.path())?;
|
||||
|
||||
Ok(temp)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Dummy Book
|
||||
|
||||
This file is just here to cause the index preprocessor to run.
|
||||
|
||||
Does a pretty good job, too.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Includes
|
||||
|
||||
{{#include ../SUMMARY.md::}}
|
||||
@@ -1,29 +0,0 @@
|
||||
# Markdown tests
|
||||
|
||||
Tests for some markdown output.
|
||||
|
||||
## Tables
|
||||
|
||||
| foo | bar |
|
||||
| --- | --- |
|
||||
| baz | bim |
|
||||
|
||||
## Footnotes
|
||||
|
||||
Footnote example[^1], or with a word[^word].
|
||||
|
||||
[^1]: This is a footnote.
|
||||
|
||||
[^word]: A longer footnote.
|
||||
With multiple lines.
|
||||
Third line.
|
||||
|
||||
## Strikethrough
|
||||
|
||||
~~strikethrough example~~
|
||||
|
||||
## Tasklisks
|
||||
|
||||
- [X] Apples
|
||||
- [X] Broccoli
|
||||
- [ ] Carrots
|
||||
@@ -1,11 +0,0 @@
|
||||
// The next line will cause a `testing` test to fail if the anchor feature is broken in such a way
|
||||
// that the whole file gets mistakenly included.
|
||||
assert!(!$TEST_STATUS);
|
||||
|
||||
// ANCHOR: myanchor
|
||||
// ANCHOR: unendinganchor
|
||||
// The next line will cause a `rendered_output` test to fail if the anchor feature is broken in
|
||||
// such a way that the content between anchors isn't included.
|
||||
// unique-string-for-anchor-test
|
||||
assert!($TEST_STATUS);
|
||||
// ANCHOR_END: myanchor
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user