mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 16:12:33 -05:00
Compare commits
196 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6dd0a4a13 | ||
|
|
432b4296ab | ||
|
|
6b1dc01a3f | ||
|
|
9d8e99f8d7 | ||
|
|
e152e197c1 | ||
|
|
7e68d01e7d | ||
|
|
bd97611eb0 | ||
|
|
8e579072b8 | ||
|
|
a918910a52 | ||
|
|
1eeb0d23e6 | ||
|
|
c842b5d06e | ||
|
|
0ac89dd826 | ||
|
|
7ec083e426 | ||
|
|
15c93b56ed | ||
|
|
ec6f26e652 | ||
|
|
cdce9a7666 | ||
|
|
63432355e6 | ||
|
|
4bf2b472bb | ||
|
|
1476ec72c3 | ||
|
|
d1c09791ab | ||
|
|
16b99be17f | ||
|
|
bcd4552bdf | ||
|
|
f942f3835e | ||
|
|
e96c608c11 | ||
|
|
e6315bf2b1 | ||
|
|
6d63343b46 | ||
|
|
e1e4518499 | ||
|
|
34d68403da | ||
|
|
e395341210 | ||
|
|
f4c54178c8 | ||
|
|
63e1dac122 | ||
|
|
e8dff6f97e | ||
|
|
534666f551 | ||
|
|
f3ee794283 | ||
|
|
94f9a9c5e0 | ||
|
|
dc6b0a6e58 | ||
|
|
2fa13cf4e0 | ||
|
|
d64a863223 | ||
|
|
1fb91d67f6 | ||
|
|
1b046e5a90 | ||
|
|
e9f5e3d7c0 | ||
|
|
c6a5d05c64 | ||
|
|
f93b2675ff | ||
|
|
365918bf89 | ||
|
|
76be253f76 | ||
|
|
0210e69abc | ||
|
|
cdbf6d2806 | ||
|
|
902ded9f89 | ||
|
|
851932bd4b | ||
|
|
f38dc687e3 | ||
|
|
391ee3bae2 | ||
|
|
ad3096e871 | ||
|
|
179bd8dcd5 | ||
|
|
426e7bee17 | ||
|
|
564c80bf6d | ||
|
|
363c12e9c3 | ||
|
|
1ffa9fe830 | ||
|
|
91d13c390a | ||
|
|
ce8d8120f8 | ||
|
|
038b5fb232 | ||
|
|
fd768efba2 | ||
|
|
481346812c | ||
|
|
3d07798832 | ||
|
|
33da0c26c4 | ||
|
|
91e94024cd | ||
|
|
c9ad6dbf10 | ||
|
|
2b455fcd34 | ||
|
|
4573f4d882 | ||
|
|
63ae0d5c18 | ||
|
|
52e406bf95 | ||
|
|
3e871d1971 | ||
|
|
84a5ba9707 | ||
|
|
44d9f4e95b | ||
|
|
d24c0ca0d7 | ||
|
|
623fc606a4 | ||
|
|
a8aee21cd0 | ||
|
|
649a021647 | ||
|
|
785ee564c5 | ||
|
|
006f99ee99 | ||
|
|
2a4e5140c9 | ||
|
|
3c10b00096 | ||
|
|
c9ddb4dd98 | ||
|
|
9822c2a178 | ||
|
|
199efd0f2c | ||
|
|
17b197620b | ||
|
|
23abd20589 | ||
|
|
7e9be8dee3 | ||
|
|
09d22e926f | ||
|
|
1696f5680e | ||
|
|
a54ee0215e | ||
|
|
9b12c5130f | ||
|
|
084771bcd5 | ||
|
|
7215d60c67 | ||
|
|
0224190ec0 | ||
|
|
ae2fc9a9d1 | ||
|
|
d65d2b2a8e | ||
|
|
69972080f0 | ||
|
|
5f2453e446 | ||
|
|
20f71af4cb | ||
|
|
efc5ee4449 | ||
|
|
14d412b279 | ||
|
|
707319e004 | ||
|
|
bdd16e25fa | ||
|
|
9a1f983e65 | ||
|
|
c2c37705e7 | ||
|
|
5f227613aa | ||
|
|
0274ad6e87 | ||
|
|
dd27c4f8ba | ||
|
|
25b9acc321 | ||
|
|
10fae8596c | ||
|
|
909bd1c54e | ||
|
|
f324aebdec | ||
|
|
5a84d641cd | ||
|
|
0b577ebd76 | ||
|
|
2056c87e28 | ||
|
|
8bfa6462f8 | ||
|
|
a8660048ca | ||
|
|
cad8988f8d | ||
|
|
3fce1151dd | ||
|
|
d23bdaa527 | ||
|
|
2f10831a80 | ||
|
|
a38a30da1e | ||
|
|
82000d917f | ||
|
|
f482aeaca3 | ||
|
|
86638abea9 | ||
|
|
a2cf838baf | ||
|
|
5bc25e32eb | ||
|
|
15c6f3f318 | ||
|
|
cb2a63ea0a | ||
|
|
50dfa365c7 | ||
|
|
3e22a5cdad | ||
|
|
5034707a73 | ||
|
|
d815b0cc52 | ||
|
|
fca149a52c | ||
|
|
b4221680e4 | ||
|
|
ba448a9dd5 | ||
|
|
aa29ef04a2 | ||
|
|
20d42a53d3 | ||
|
|
8c8f0a4dbf | ||
|
|
6904653a82 | ||
|
|
74e01ea6e3 | ||
|
|
0732cb47b9 | ||
|
|
29338b5ade | ||
|
|
4019060ef4 | ||
|
|
3e1d750efa | ||
|
|
41bfbc69e6 | ||
|
|
6fdd7b4a17 | ||
|
|
c6d9f15cba | ||
|
|
0f397ebdb5 | ||
|
|
342b6ee7b5 | ||
|
|
9952ac15a5 | ||
|
|
7add0dbf10 | ||
|
|
03470a7531 | ||
|
|
dd778d50f9 | ||
|
|
ac3e4b6c1e | ||
|
|
3706ddc5cc | ||
|
|
adcea9b3b9 | ||
|
|
ba8107120c | ||
|
|
b9e433710d | ||
|
|
f10d23e893 | ||
|
|
12b4a9631a | ||
|
|
36fa0064de | ||
|
|
566a42c4f7 | ||
|
|
6e143ce2a1 | ||
|
|
46963ebf65 | ||
|
|
c948fe4d6a | ||
|
|
b0ef5a54cc | ||
|
|
fbc875dd9f | ||
|
|
e6b1413d22 | ||
|
|
1d3b99c0df | ||
|
|
8181445d99 | ||
|
|
14aeb0cb83 | ||
|
|
3e6b42cfba | ||
|
|
c57a8fcfc4 | ||
|
|
9d6fcc9afe | ||
|
|
ea8f0f6161 | ||
|
|
06e8f6f849 | ||
|
|
36e5525ea5 | ||
|
|
e5d5f5d02b | ||
|
|
98088c91dd | ||
|
|
a4f7d11e92 | ||
|
|
4c7e85ba82 | ||
|
|
9693c4af05 | ||
|
|
3052fe3827 | ||
|
|
c85c3eb292 | ||
|
|
0d734bbb03 | ||
|
|
b9b34f97d9 | ||
|
|
ee59e22603 | ||
|
|
22a6dca69b | ||
|
|
4f698f813c | ||
|
|
97f1948681 | ||
|
|
7acc7a03a8 | ||
|
|
0ed1cbe486 | ||
|
|
2c382a58d3 | ||
|
|
1bc2ebd775 | ||
|
|
0fb814c6d6 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -6,3 +6,5 @@
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.png binary
|
||||
*.eot binary
|
||||
*.woff2 binary
|
||||
|
||||
28
.github/workflows/main.yml
vendored
28
.github/workflows/main.yml
vendored
@@ -40,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:
|
||||
@@ -88,6 +88,28 @@ jobs:
|
||||
- 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
|
||||
@@ -99,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/
|
||||
|
||||
95
CHANGELOG.md
95
CHANGELOG.md
@@ -1,5 +1,100 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.4.52
|
||||
[v0.4.51...v0.4.52](https://github.com/rust-lang/mdBook/compare/v0.4.51...v0.4.52)
|
||||
|
||||
**Note:** If you have a custom `index.hbs` theme file, it is recommended that you update it to the latest version to pick up the fixes in this release.
|
||||
|
||||
### Added
|
||||
- Added the ability to redirect `#` HTML fragments using the existing `output.html.redirect` table.
|
||||
[#2747](https://github.com/rust-lang/mdBook/pull/2747)
|
||||
- Added the `rel="edit"` attribute to the edit page button.
|
||||
[#2702](https://github.com/rust-lang/mdBook/pull/2702)
|
||||
|
||||
### Changed
|
||||
- The search index is now only loaded when the search input is opened instead of always being loaded.
|
||||
[#2553](https://github.com/rust-lang/mdBook/pull/2553)
|
||||
[#2735](https://github.com/rust-lang/mdBook/pull/2735)
|
||||
- The `mdbook serve` command has switched its underlying server library from warp to axum.
|
||||
[#2748](https://github.com/rust-lang/mdBook/pull/2748)
|
||||
- Updated dependencies.
|
||||
[#2752](https://github.com/rust-lang/mdBook/pull/2752)
|
||||
|
||||
### Fixed
|
||||
- The sidebar is now set to `display:none` when it is hidden in order to prevent the browser's search from thinking the sidebar's text is visible.
|
||||
[#2725](https://github.com/rust-lang/mdBook/pull/2725)
|
||||
- Fixed search index URL not updating correctly when `hash-files` is enabled.
|
||||
[#2742](https://github.com/rust-lang/mdBook/pull/2742)
|
||||
[#2746](https://github.com/rust-lang/mdBook/pull/2746)
|
||||
- Fixed several sidebar animation bugs, particularly when manually resizing.
|
||||
[#2750](https://github.com/rust-lang/mdBook/pull/2750)
|
||||
|
||||
## mdBook 0.4.51
|
||||
[v0.4.50...v0.4.51](https://github.com/rust-lang/mdBook/compare/v0.4.50...v0.4.51)
|
||||
|
||||
### Fixed
|
||||
- Fixed regression that broke the `S` search hotkey.
|
||||
[#2713](https://github.com/rust-lang/mdBook/pull/2713)
|
||||
|
||||
## mdBook 0.4.50
|
||||
[v0.4.49...v0.4.50](https://github.com/rust-lang/mdBook/compare/v0.4.49...v0.4.50)
|
||||
|
||||
### Added
|
||||
|
||||
- Added a keyboard shortcut help popup when pressing `?`.
|
||||
[#2608](https://github.com/rust-lang/mdBook/pull/2608)
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the look of the sidebar resize handle to match the new rustdoc format.
|
||||
[#2691](https://github.com/rust-lang/mdBook/pull/2691)
|
||||
- `/` can now be used to open the search bar.
|
||||
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
|
||||
- Pressing enter from the search bar will navigate to the first entry.
|
||||
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
|
||||
- Updated `opener` to drop some dependencies.
|
||||
[#2709](https://github.com/rust-lang/mdBook/pull/2709)
|
||||
- Updated dependencies, MSRV raised to 1.82.
|
||||
[#2711](https://github.com/rust-lang/mdBook/pull/2711)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed uncaught exception when pressing down when there are no search results.
|
||||
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
|
||||
- Fixed syntax highlighting of Rust code in the ACE editor.
|
||||
[#2710](https://github.com/rust-lang/mdBook/pull/2710)
|
||||
|
||||
## mdBook 0.4.49
|
||||
[v0.4.48...v0.4.49](https://github.com/rust-lang/mdBook/compare/v0.4.48...v0.4.49)
|
||||
|
||||
### Added
|
||||
|
||||
- Added a warning on unused fields in the root of `book.toml`.
|
||||
[#2622](https://github.com/rust-lang/mdBook/pull/2622)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated dependencies.
|
||||
[#2650](https://github.com/rust-lang/mdBook/pull/2650)
|
||||
[#2688](https://github.com/rust-lang/mdBook/pull/2688)
|
||||
- Updated minimum Rust version to 1.81.
|
||||
[#2688](https://github.com/rust-lang/mdBook/pull/2688)
|
||||
- The unused `book.multilingual` field is no longer serialized, or shown in `mdbook init`.
|
||||
[#2689](https://github.com/rust-lang/mdBook/pull/2689)
|
||||
- Speed up search index loading by using `JSON.parse` instead of parsing JavaScript.
|
||||
[#2633](https://github.com/rust-lang/mdBook/pull/2633)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Search highlighting will not try to highlight in SVG `<text>` elements because it breaks the element.
|
||||
[#2668](https://github.com/rust-lang/mdBook/pull/2668)
|
||||
- Fixed scrolling of the sidebar when a search highlight term is in the URL.
|
||||
[#2675](https://github.com/rust-lang/mdBook/pull/2675)
|
||||
- Fixed issues when multiple footnote definitions use the same ID. Now, only one definition is used, and a warning is displayed.
|
||||
[#2681](https://github.com/rust-lang/mdBook/pull/2681)
|
||||
- The sidebar is now restricted to 80% of the viewport width to make it possible to collapse it when the viewport is very narrow.
|
||||
[#2679](https://github.com/rust-lang/mdBook/pull/2679)
|
||||
|
||||
## mdBook 0.4.48
|
||||
[v0.4.47...v0.4.48](https://github.com/rust-lang/mdBook/compare/v0.4.47...v0.4.48)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
1542
Cargo.lock
generated
1542
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
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.48"
|
||||
version = "0.4.52"
|
||||
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,25 +55,25 @@ walkdir = { version = "2.3.3", optional = true }
|
||||
|
||||
# Serve feature
|
||||
futures-util = { version = "0.3.28", optional = true }
|
||||
tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.6", default-features = false, features = ["websocket"], optional = true }
|
||||
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
axum = { version = "0.8.0", features = ["ws"], optional = true }
|
||||
tower-http = { version = "0.6.0", features = ["fs", "trace"], optional = true }
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "3.0.2", optional = true }
|
||||
ammonia = { version = "4.0.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.11"
|
||||
predicates = "3.0.3"
|
||||
select = "0.6.0"
|
||||
semver = "1.0.17"
|
||||
snapbox = { version = "0.6.21", features = ["diff", "dir", "term-svg", "regex", "json"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
walkdir = "2.3.3"
|
||||
|
||||
[features]
|
||||
default = ["watch", "serve", "search"]
|
||||
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"]
|
||||
serve = ["dep:futures-util", "dep:tokio", "dep:warp"]
|
||||
serve = ["dep:futures-util", "dep:tokio", "dep:axum", "dep:tower-http"]
|
||||
search = ["dep:elasticlunr-rs", "dep:ammonia"]
|
||||
|
||||
[[bin]]
|
||||
@@ -91,3 +96,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.48/mdbook-v0.4.48-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.52/mdbook-v0.4.52-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
bin/mdbook build
|
||||
```
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ edition = "2015" # the default edition for code blocks
|
||||
|
||||
- **edition**: Rust edition to use by default for the code snippets. Default
|
||||
is `"2015"`. Individual code blocks can be controlled with the `edition2015`,
|
||||
`edition2018` or `edition2021` annotations, such as:
|
||||
`edition2018`, `edition2021` or `edition2024` annotations, such as:
|
||||
|
||||
~~~text
|
||||
```rust,edition2015
|
||||
|
||||
@@ -310,13 +310,21 @@ This is useful when you move, rename, or remove a page to ensure that links to t
|
||||
[output.html.redirect]
|
||||
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
|
||||
"/other-installation-methods.html" = "../infra/other-installation-methods.html"
|
||||
|
||||
# Fragment redirects also work.
|
||||
"/some-existing-page.html#old-fragment" = "some-existing-page.html#new-fragment"
|
||||
|
||||
# Fragment redirects also work for deleted pages.
|
||||
"/old-page.html" = "new-page.html"
|
||||
"/old-page.html#old-fragment" = "new-page.html#new-fragment"
|
||||
```
|
||||
|
||||
The table contains key-value pairs where the key is where the redirect file needs to be created, as an absolute path from the build directory, (e.g. `/appendices/bibliography.html`).
|
||||
The value can be any valid URI the browser should navigate to (e.g. `https://rust-lang.org/`, `/overview.html`, or `../bibliography.html`).
|
||||
|
||||
This will generate an HTML page which will automatically redirect to the given location.
|
||||
Note that the source location does not support `#` anchor redirects.
|
||||
|
||||
When fragment redirects are specified, the page must use JavaScript to redirect to the correct location. This is useful if you rename or move a section header. Fragment redirects work with existing pages and deleted pages.
|
||||
|
||||
## Markdown Renderer
|
||||
|
||||
|
||||
@@ -75,15 +75,22 @@ Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
Read about [mdBook](mdbook.md).
|
||||
|
||||
And now [an mdBook link] that is not inline, unlike the above.
|
||||
|
||||
A bare url: <https://www.rust-lang.org>.
|
||||
|
||||
[an mdBook link]: https://github.com/rust-lang/mdBook
|
||||
```
|
||||
|
||||
Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
Read about [mdBook](mdbook.md).
|
||||
|
||||
And now [an mdBook link] that is not inline, unlike the above.
|
||||
|
||||
A bare url: <https://www.rust-lang.org>.
|
||||
|
||||
[an mdBook link]: https://github.com/rust-lang/mdBook
|
||||
----
|
||||
|
||||
Relative links that end with `.md` will be converted to the `.html` extension.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"browser-ui-test": "0.19.0",
|
||||
"browser-ui-test": "0.21.1",
|
||||
"eslint": "^8.57.1"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/front-end/*js src/front-end/**/*js",
|
||||
"lint-fix": "eslint --fix src/front-end/*js src/front-end/**/*js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,10 +656,10 @@ And here is some \
|
||||
let got = load_book(&temp_dir, &cfg);
|
||||
assert!(got.is_err());
|
||||
let error_message = got.err().unwrap().to_string();
|
||||
let expeceted = format!(
|
||||
let expected = format!(
|
||||
r#"Couldn't open SUMMARY.md in {:?} directory"#,
|
||||
temp_dir.path()
|
||||
);
|
||||
assert_eq!(error_message, expeceted);
|
||||
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);
|
||||
|
||||
@@ -248,7 +248,7 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
let mut files = HashSet::new();
|
||||
for part in [&prefix_chapters, &numbered_chapters, &suffix_chapters] {
|
||||
self.check_for_duplicates(&part, &mut files)?;
|
||||
Self::check_for_duplicates(&part, &mut files)?;
|
||||
}
|
||||
|
||||
Ok(Summary {
|
||||
@@ -261,7 +261,6 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
/// Recursively check for duplicate files in the summary items.
|
||||
fn check_for_duplicates<'b>(
|
||||
&self,
|
||||
items: &'b [SummaryItem],
|
||||
files: &mut HashSet<&'b PathBuf>,
|
||||
) -> Result<()> {
|
||||
@@ -276,7 +275,7 @@ impl<'a> SummaryParser<'a> {
|
||||
}
|
||||
}
|
||||
// Recursively check nested items
|
||||
self.check_for_duplicates(&link.nested_items, files)?;
|
||||
Self::check_for_duplicates(&link.nested_items, files)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -2,6 +2,9 @@ use super::command_prelude::*;
|
||||
#[cfg(feature = "watch")]
|
||||
use super::watch;
|
||||
use crate::{get_book_dir, open};
|
||||
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use clap::builder::NonEmptyStringValueParser;
|
||||
use futures_util::sink::SinkExt;
|
||||
use futures_util::StreamExt;
|
||||
@@ -11,8 +14,7 @@ use mdbook::MDBook;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::broadcast;
|
||||
use warp::ws::Message;
|
||||
use warp::Filter;
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
|
||||
/// The HTTP endpoint for the websocket used to trigger reloads when a file changes.
|
||||
const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
|
||||
@@ -116,32 +118,19 @@ async fn serve(
|
||||
reload_tx: broadcast::Sender<Message>,
|
||||
file_404: &str,
|
||||
) {
|
||||
// A warp Filter which captures `reload_tx` and provides an `rx` copy to
|
||||
// receive reload messages.
|
||||
let sender = warp::any().map(move || reload_tx.subscribe());
|
||||
let reload_tx_clone = reload_tx.clone();
|
||||
|
||||
// A warp Filter to handle the livereload endpoint. This upgrades to a
|
||||
// websocket, and then waits for any filesystem change notifications, and
|
||||
// relays them over the websocket.
|
||||
let livereload = warp::path(LIVE_RELOAD_ENDPOINT)
|
||||
.and(warp::ws())
|
||||
.and(sender)
|
||||
.map(|ws: warp::ws::Ws, mut rx: broadcast::Receiver<Message>| {
|
||||
ws.on_upgrade(move |ws| async move {
|
||||
let (mut user_ws_tx, _user_ws_rx) = ws.split();
|
||||
trace!("websocket got connection");
|
||||
if let Ok(m) = rx.recv().await {
|
||||
trace!("notify of reload");
|
||||
let _ = user_ws_tx.send(m).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
// A warp Filter that serves from the filesystem.
|
||||
let book_route = warp::fs::dir(build_dir.clone());
|
||||
// The fallback route for 404 errors
|
||||
let fallback_route = warp::fs::file(build_dir.join(file_404))
|
||||
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
|
||||
let routes = livereload.or(book_route).or(fallback_route);
|
||||
// WebSocket handler for live reload
|
||||
let websocket_handler = move |ws: WebSocketUpgrade| async move {
|
||||
let reload_tx = reload_tx_clone.clone();
|
||||
ws.on_upgrade(move |socket| websocket_connection(socket, reload_tx))
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route(&format!("/{LIVE_RELOAD_ENDPOINT}"), get(websocket_handler))
|
||||
.fallback_service(
|
||||
ServeDir::new(&build_dir).not_found_service(ServeFile::new(build_dir.join(file_404))),
|
||||
);
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// exit if serve panics
|
||||
@@ -149,5 +138,20 @@ async fn serve(
|
||||
std::process::exit(1);
|
||||
}));
|
||||
|
||||
warp::serve(routes).run(address).await;
|
||||
let listener = tokio::net::TcpListener::bind(&address)
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("Unable to bind to {address}: {e}"));
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn websocket_connection(ws: WebSocket, reload_tx: broadcast::Sender<Message>) {
|
||||
let (mut user_ws_tx, _user_ws_rx) = ws.split();
|
||||
let mut rx = reload_tx.subscribe();
|
||||
|
||||
trace!("websocket got connection");
|
||||
if let Ok(m) = rx.recv().await {
|
||||
trace!("notify of reload");
|
||||
let _ = user_ws_tx.send(m).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +312,8 @@ impl<'de> serde::Deserialize<'de> for Config {
|
||||
return Ok(Config::from_legacy(raw));
|
||||
}
|
||||
|
||||
warn_on_invalid_fields(&raw);
|
||||
|
||||
use serde::de::Error;
|
||||
let mut table = match raw {
|
||||
Value::Table(t) => t,
|
||||
@@ -376,6 +378,17 @@ fn parse_env(key: &str) -> Option<String> {
|
||||
.map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
|
||||
}
|
||||
|
||||
fn warn_on_invalid_fields(table: &Value) {
|
||||
let valid_items = ["book", "build", "rust", "output", "preprocessor"];
|
||||
|
||||
let table = table.as_table().expect("root must be a table");
|
||||
for item in table.keys() {
|
||||
if !valid_items.contains(&item.as_str()) {
|
||||
warn!("Invalid field {:?} in book.toml", &item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_legacy_format(table: &Value) -> bool {
|
||||
let legacy_items = [
|
||||
"title",
|
||||
@@ -408,6 +421,9 @@ pub struct BookConfig {
|
||||
/// Location of the book source relative to the book's root directory.
|
||||
pub src: PathBuf,
|
||||
/// Does this book support more than one language?
|
||||
// TODO: Remove this field in 0.5, it is unused:
|
||||
// https://github.com/rust-lang/mdBook/issues/2636
|
||||
#[serde(skip_serializing)]
|
||||
pub multilingual: bool,
|
||||
/// The main language of the book.
|
||||
pub language: Option<String>,
|
||||
|
||||
@@ -344,9 +344,34 @@ mark.fade-out {
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
|
||||
#searchbar-outer.searching #searchbar {
|
||||
padding-right: 30px;
|
||||
}
|
||||
#searchbar-outer .spinner-wrapper {
|
||||
display: none;
|
||||
}
|
||||
#searchbar-outer.searching .spinner-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spinner-wrapper {
|
||||
--spinner-margin: 2px;
|
||||
position: absolute;
|
||||
margin-block-start: calc(var(--searchbar-margin-block-start) + var(--spinner-margin));
|
||||
right: var(--spinner-margin);
|
||||
top: 0;
|
||||
bottom: var(--spinner-margin);
|
||||
padding: 6px;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
#searchbar {
|
||||
width: 100%;
|
||||
margin-block-start: 5px;
|
||||
margin-block-start: var(--searchbar-margin-block-start);
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
@@ -474,9 +499,24 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
|
||||
.sidebar-resize-handle .sidebar-resize-indicator {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background-color: var(--icons);
|
||||
height: 16px;
|
||||
color: var(--icons);
|
||||
margin-inline-start: var(--sidebar-resize-indicator-space);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.sidebar-resize-handle .sidebar-resize-indicator::before {
|
||||
content: "";
|
||||
width: 2px;
|
||||
height: 12px;
|
||||
border-left: dotted 2px currentColor;
|
||||
}
|
||||
.sidebar-resize-handle .sidebar-resize-indicator::after {
|
||||
content: "";
|
||||
width: 2px;
|
||||
height: 16px;
|
||||
border-left: dotted 2px currentColor;
|
||||
}
|
||||
|
||||
[dir=rtl] .sidebar .sidebar-resize-handle {
|
||||
@@ -490,7 +530,6 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
/* sidebar-hidden */
|
||||
#sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
z-index: -1;
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
@@ -641,3 +680,46 @@ html:not(.sidebar-resizing) .sidebar {
|
||||
margin-inline-start: -14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
/* The container for the help popup that covers the whole window. */
|
||||
#mdbook-help-container {
|
||||
/* Position and size for the whole window. */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* This uses flex layout (which is set in book.js), and centers the popup
|
||||
in the window.*/
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
/* Dim out the book while the popup is visible. */
|
||||
background: var(--overlay-bg);
|
||||
}
|
||||
|
||||
/* The popup help box. */
|
||||
#mdbook-help-popup {
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
border-width: 1px;
|
||||
border-color: var(--theme-popup-border);
|
||||
border-style: solid;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mdbook-help-title {
|
||||
text-align: center;
|
||||
/* mdbook's margin for h2 is way too large. */
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
@@ -86,11 +86,12 @@ h6:target::before {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
.no-js .page-wrapper,
|
||||
html:not(.js) .page-wrapper,
|
||||
.js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
[dir=rtl] .js:not(.sidebar-resizing) .page-wrapper {
|
||||
[dir=rtl]:not(.js) .page-wrapper,
|
||||
[dir=rtl].js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -10,6 +11,7 @@
|
||||
--menu-bar-height: 50px;
|
||||
--mono-font: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
--code-font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
--searchbar-margin-block-start: 5px;
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
@@ -63,6 +65,8 @@
|
||||
--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 {
|
||||
@@ -114,6 +118,8 @@
|
||||
--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) {
|
||||
@@ -165,6 +171,8 @@
|
||||
--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 {
|
||||
@@ -216,6 +224,8 @@
|
||||
--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 {
|
||||
@@ -265,6 +275,8 @@
|
||||
--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) {
|
||||
|
||||
@@ -515,17 +515,39 @@ aria-label="Show hidden lines"></button>';
|
||||
})();
|
||||
|
||||
(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');
|
||||
const sidebarCheckbox = document.getElementById('sidebar-toggle-anchor');
|
||||
let firstContact = null;
|
||||
|
||||
|
||||
/* Because we cannot change the `display` using only CSS after/before the transition, we
|
||||
need JS to do it. We change the display to prevent the browsers search to find text inside
|
||||
the collapsed sidebar. */
|
||||
if (!document.documentElement.classList.contains('sidebar-visible')) {
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
sidebar.addEventListener('transitionend', () => {
|
||||
/* We only change the display to "none" if we're collapsing the sidebar. */
|
||||
if (!sidebarCheckbox.checked) {
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
});
|
||||
sidebarToggleButton.addEventListener('click', () => {
|
||||
/* To allow the sidebar expansion animation, we first need to put back the display. */
|
||||
if (!sidebarCheckbox.checked) {
|
||||
sidebar.style.display = '';
|
||||
// Workaround for Safari skipping the animation when changing
|
||||
// `display` and a transform in the same event loop. This forces a
|
||||
// reflow after updating the display.
|
||||
sidebar.offsetHeight;
|
||||
}
|
||||
});
|
||||
|
||||
function showSidebar() {
|
||||
body.classList.remove('sidebar-hidden');
|
||||
body.classList.add('sidebar-visible');
|
||||
document.documentElement.classList.add('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', 0);
|
||||
});
|
||||
@@ -539,8 +561,7 @@ aria-label="Show hidden lines"></button>';
|
||||
}
|
||||
|
||||
function hideSidebar() {
|
||||
body.classList.remove('sidebar-visible');
|
||||
body.classList.add('sidebar-hidden');
|
||||
document.documentElement.classList.remove('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', -1);
|
||||
});
|
||||
@@ -554,12 +575,12 @@ aria-label="Show hidden lines"></button>';
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
sidebarToggleAnchor.addEventListener('change', function sidebarToggle() {
|
||||
if (sidebarToggleAnchor.checked) {
|
||||
sidebarCheckbox.addEventListener('change', function sidebarToggle() {
|
||||
if (sidebarCheckbox.checked) {
|
||||
const current_width = parseInt(
|
||||
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
|
||||
document.documentElement.style.getPropertyValue('--sidebar-target-width'), 10);
|
||||
if (current_width < 150) {
|
||||
document.documentElement.style.setProperty('--sidebar-width', '150px');
|
||||
document.documentElement.style.setProperty('--sidebar-target-width', '150px');
|
||||
}
|
||||
showSidebar();
|
||||
} else {
|
||||
@@ -572,23 +593,23 @@ aria-label="Show hidden lines"></button>';
|
||||
function initResize() {
|
||||
window.addEventListener('mousemove', resize, false);
|
||||
window.addEventListener('mouseup', stopResize, false);
|
||||
body.classList.add('sidebar-resizing');
|
||||
document.documentElement.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
let pos = e.clientX - sidebar.offsetLeft;
|
||||
if (pos < 20) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (body.classList.contains('sidebar-hidden')) {
|
||||
if (!document.documentElement.classList.contains('sidebar-visible')) {
|
||||
showSidebar();
|
||||
}
|
||||
pos = Math.min(pos, window.innerWidth - 100);
|
||||
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
|
||||
document.documentElement.style.setProperty('--sidebar-target-width', pos + 'px');
|
||||
}
|
||||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize() {
|
||||
body.classList.remove('sidebar-resizing');
|
||||
document.documentElement.classList.remove('sidebar-resizing');
|
||||
window.removeEventListener('mousemove', resize, false);
|
||||
window.removeEventListener('mouseup', stopResize, false);
|
||||
}
|
||||
@@ -623,7 +644,7 @@ aria-label="Show hidden lines"></button>';
|
||||
|
||||
(function chapterNavigation() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
if (window.search && window.search.hasFocus()) {
|
||||
@@ -643,6 +664,55 @@ aria-label="Show hidden lines"></button>';
|
||||
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();
|
||||
@@ -716,7 +786,7 @@ aria-label="Show hidden lines"></button>';
|
||||
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).
|
||||
// When the script loads, the page can be at any scroll (e.g. if you refresh it).
|
||||
menu.style.top = scrollTop + 'px';
|
||||
// Same as parseInt(menu.style.top.slice(0, -2), but faster
|
||||
let topCache = menu.style.top.slice(0, -2);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/* 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
|
||||
@@ -22,6 +22,7 @@ window.search = window.search || {};
|
||||
}
|
||||
|
||||
const search_wrap = document.getElementById('search-wrapper'),
|
||||
searchbar_outer = document.getElementById('searchbar-outer'),
|
||||
searchbar = document.getElementById('searchbar'),
|
||||
searchresults = document.getElementById('searchresults'),
|
||||
searchresults_outer = document.getElementById('searchresults-outer'),
|
||||
@@ -29,16 +30,11 @@ window.search = window.search || {};
|
||||
searchicon = document.getElementById('search-toggle'),
|
||||
content = document.getElementById('content'),
|
||||
|
||||
mark_exclude = [],
|
||||
// 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',
|
||||
|
||||
SEARCH_HOTKEY_KEYCODE = 83,
|
||||
ESCAPE_KEYCODE = 27,
|
||||
DOWN_KEYCODE = 40,
|
||||
UP_KEYCODE = 38,
|
||||
SELECT_KEYCODE = 13;
|
||||
URL_MARK_PARAM = 'highlight';
|
||||
|
||||
let current_searchterm = '',
|
||||
doc_urls = [],
|
||||
@@ -267,6 +263,18 @@ window.search = window.search || {};
|
||||
doc_urls = config.doc_urls;
|
||||
searchindex = elasticlunr.Index.load(config.index);
|
||||
|
||||
searchbar_outer.classList.remove('searching');
|
||||
|
||||
searchbar.focus();
|
||||
|
||||
const searchterm = searchbar.value.trim();
|
||||
if (searchterm !== '') {
|
||||
searchbar.classList.add('active');
|
||||
doSearch(searchterm);
|
||||
}
|
||||
}
|
||||
|
||||
function initSearchInteractions(config) {
|
||||
// Set up events
|
||||
searchicon.addEventListener('click', () => {
|
||||
searchIconClickHandler();
|
||||
@@ -288,8 +296,13 @@ window.search = window.search || {};
|
||||
|
||||
// If reloaded, do the search or mark again, depending on the current url parameters
|
||||
doSearchOrMarkFromUrl();
|
||||
|
||||
// Exported functions
|
||||
config.hasFocus = hasFocus;
|
||||
}
|
||||
|
||||
initSearchInteractions(window.search);
|
||||
|
||||
function unfocusSearchbar() {
|
||||
// hacky, but just focusing a div only works once
|
||||
const tmp = document.createElement('input');
|
||||
@@ -348,7 +361,7 @@ window.search = window.search || {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.keyCode === ESCAPE_KEYCODE) {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
searchbar.classList.remove('active');
|
||||
setSearchUrlParameters('',
|
||||
@@ -358,31 +371,38 @@ window.search = window.search || {};
|
||||
}
|
||||
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)) {
|
||||
const first = searchresults.firstElementChild;
|
||||
if (first !== null) {
|
||||
unfocusSearchbar();
|
||||
first.classList.add('focus');
|
||||
if (e.key === 'Enter') {
|
||||
window.location.assign(first.querySelector('a'));
|
||||
}
|
||||
}
|
||||
} else if (!hasFocus() && (e.key === 'ArrowDown'
|
||||
|| e.key === 'ArrowUp'
|
||||
|| e.key === 'Enter')) {
|
||||
// not `:focus` because browser does annoying scrolling
|
||||
const focused = searchresults.querySelector('li.focus');
|
||||
if (!focused) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (e.keyCode === DOWN_KEYCODE) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
const next = focused.nextElementSibling;
|
||||
if (next) {
|
||||
focused.classList.remove('focus');
|
||||
next.classList.add('focus');
|
||||
}
|
||||
} else if (e.keyCode === UP_KEYCODE) {
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
focused.classList.remove('focus');
|
||||
const prev = focused.previousElementSibling;
|
||||
if (prev) {
|
||||
@@ -390,14 +410,34 @@ window.search = window.search || {};
|
||||
} else {
|
||||
searchbar.select();
|
||||
}
|
||||
} else { // SELECT_KEYCODE
|
||||
} else { // Enter
|
||||
window.location.assign(focused.querySelector('a'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadSearchScript(url, id) {
|
||||
if (document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
searchbar_outer.classList.add('searching');
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.id = id;
|
||||
script.onload = () => init(window.search);
|
||||
script.onerror = error => {
|
||||
console.error(`Failed to load \`${url}\`: ${error}`);
|
||||
};
|
||||
document.head.append(script);
|
||||
}
|
||||
|
||||
function showSearch(yes) {
|
||||
if (yes) {
|
||||
loadSearchScript(
|
||||
window.path_to_searchindex_js ||
|
||||
path_to_root + '{{ resource "searchindex.js" }}',
|
||||
'search-index');
|
||||
search_wrap.classList.remove('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
@@ -480,14 +520,14 @@ window.search = window.search || {};
|
||||
// Don't search the same twice
|
||||
if (current_searchterm === searchterm) {
|
||||
return;
|
||||
} else {
|
||||
current_searchterm = searchterm;
|
||||
}
|
||||
|
||||
searchbar_outer.classList.add('searching');
|
||||
if (searchindex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
current_searchterm = searchterm;
|
||||
|
||||
// Do the actual search
|
||||
const results = searchindex.search(searchterm, search_options);
|
||||
const resultcount = Math.min(results.length, results_options.limit_results);
|
||||
@@ -506,21 +546,9 @@ window.search = window.search || {};
|
||||
|
||||
// Display results
|
||||
showResults(true);
|
||||
searchbar_outer.classList.remove('searching');
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -58,11 +58,27 @@
|
||||
const path_to_root = "{{ path_to_root }}";
|
||||
const default_light_theme = "{{ default_theme }}";
|
||||
const default_dark_theme = "{{ preferred_dark_theme }}";
|
||||
{{#if search_js}}
|
||||
window.path_to_searchindex_js = "{{ resource "searchindex.js" }}";
|
||||
{{/if}}
|
||||
</script>
|
||||
<!-- Start loading toc.js asap -->
|
||||
<script src="{{ resource "toc.js" }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mdbook-help-container">
|
||||
<div id="mdbook-help-popup">
|
||||
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
|
||||
<div>
|
||||
<p>Press <kbd>←</kbd> or <kbd>→</kbd> to navigate between chapters</p>
|
||||
{{#if search_enabled}}
|
||||
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
|
||||
{{/if}}
|
||||
<p>Press <kbd>?</kbd> to show this help</p>
|
||||
<p>Press <kbd>Esc</kbd> to hide this help</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="body-container">
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script>
|
||||
@@ -103,10 +119,13 @@
|
||||
sidebar = sidebar || 'visible';
|
||||
} else {
|
||||
sidebar = 'hidden';
|
||||
sidebar_toggle.checked = false;
|
||||
}
|
||||
if (sidebar === 'visible') {
|
||||
sidebar_toggle.checked = true;
|
||||
} else {
|
||||
html.classList.remove('sidebar-visible');
|
||||
}
|
||||
sidebar_toggle.checked = sidebar === 'visible';
|
||||
html.classList.remove('sidebar-visible');
|
||||
html.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
@@ -142,7 +161,7 @@
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="searchbar">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
{{/if}}
|
||||
@@ -162,7 +181,7 @@
|
||||
</a>
|
||||
{{/if}}
|
||||
{{#if git_repository_edit_url}}
|
||||
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit">
|
||||
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit" rel="edit">
|
||||
<i id="git-edit-button" class="fa fa-edit"></i>
|
||||
</a>
|
||||
{{/if}}
|
||||
@@ -173,7 +192,12 @@
|
||||
{{#if search_enabled}}
|
||||
<div id="search-wrapper" class="hidden">
|
||||
<form id="searchbar-outer" class="searchbar-outer">
|
||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
<div class="search-wrapper">
|
||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
<div class="spinner-wrapper">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||
<div id="searchresults-header" class="searchresults-header"></div>
|
||||
@@ -284,8 +308,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}}
|
||||
@@ -323,6 +347,21 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if fragment_map}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fragmentMap =
|
||||
{{{fragment_map}}}
|
||||
;
|
||||
const target = fragmentMap[window.location.hash];
|
||||
if (target) {
|
||||
let url = new URL(target, window.location.href);
|
||||
window.location.replace(url.href);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,5 +8,29 @@
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
|
||||
|
||||
<script>
|
||||
// This handles redirects that involve fragments.
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fragmentMap =
|
||||
{{{fragment_map}}}
|
||||
;
|
||||
const fragment = window.location.hash;
|
||||
if (fragment) {
|
||||
let redirectUrl = "{{url}}";
|
||||
const target = fragmentMap[fragment];
|
||||
if (target) {
|
||||
let url = new URL(target, window.location.href);
|
||||
redirectUrl = url.href;
|
||||
} else {
|
||||
let url = new URL(redirectUrl, window.location.href);
|
||||
url.hash = window.location.hash;
|
||||
redirectUrl = url.href;
|
||||
}
|
||||
window.location.replace(redirectUrl);
|
||||
}
|
||||
// else redirect handled by http-equiv
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -109,6 +111,14 @@ impl HtmlHandlebars {
|
||||
.insert("section".to_owned(), json!(section.to_string()));
|
||||
}
|
||||
|
||||
let redirects = collect_redirects_for_path(&filepath, &ctx.html_config.redirect)?;
|
||||
if !redirects.is_empty() {
|
||||
ctx.data.insert(
|
||||
"fragment_map".to_owned(),
|
||||
json!(serde_json::to_string(&redirects)?),
|
||||
);
|
||||
}
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
@@ -207,7 +217,6 @@ impl HtmlHandlebars {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
fn post_process(
|
||||
&self,
|
||||
rendered: String,
|
||||
@@ -265,15 +274,27 @@ impl HtmlHandlebars {
|
||||
}
|
||||
|
||||
log::debug!("Emitting redirects");
|
||||
let redirects = combine_fragment_redirects(redirects);
|
||||
|
||||
for (original, new) in redirects {
|
||||
log::debug!("Redirecting \"{}\" → \"{}\"", original, new);
|
||||
for (original, (dest, fragment_map)) in redirects {
|
||||
// Note: all paths are relative to the build directory, so the
|
||||
// leading slash in an absolute path means nothing (and would mess
|
||||
// up `root.join(original)`).
|
||||
let original = original.trim_start_matches('/');
|
||||
let filename = root.join(original);
|
||||
self.emit_redirect(handlebars, &filename, new)?;
|
||||
if filename.exists() {
|
||||
// This redirect is handled by the in-page fragment mapper.
|
||||
continue;
|
||||
}
|
||||
if dest.is_empty() {
|
||||
bail!(
|
||||
"redirect entry for `{original}` only has source paths with `#` fragments\n\
|
||||
There must be an entry without the `#` fragment to determine the default \
|
||||
destination."
|
||||
);
|
||||
}
|
||||
log::debug!("Redirecting \"{}\" → \"{}\"", original, dest);
|
||||
self.emit_redirect(handlebars, &filename, &dest, &fragment_map)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -284,23 +305,17 @@ impl HtmlHandlebars {
|
||||
handlebars: &Handlebars<'_>,
|
||||
original: &Path,
|
||||
destination: &str,
|
||||
fragment_map: &BTreeMap<String, String>,
|
||||
) -> Result<()> {
|
||||
if original.exists() {
|
||||
// sanity check to avoid accidentally overwriting a real file.
|
||||
let msg = format!(
|
||||
"Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?",
|
||||
original.display(),
|
||||
destination,
|
||||
);
|
||||
return Err(Error::msg(msg));
|
||||
}
|
||||
|
||||
if let Some(parent) = original.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
|
||||
}
|
||||
|
||||
let js_map = serde_json::to_string(fragment_map)?;
|
||||
|
||||
let ctx = json!({
|
||||
"fragment_map": js_map,
|
||||
"url": destination,
|
||||
});
|
||||
let f = File::create(original)?;
|
||||
@@ -663,10 +678,10 @@ fn make_data(
|
||||
/// Goes through the rendered HTML, making sure all header tags have
|
||||
/// an anchor respectively so people can link to sections directly.
|
||||
fn build_header_links(html: &str) -> String {
|
||||
static BUILD_HEADER_LINKS: Lazy<Regex> = Lazy::new(|| {
|
||||
static BUILD_HEADER_LINKS: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r#"<h(\d)(?: id="([^"]+)")?(?: class="([^"]+)")?>(.*?)</h\d>"#).unwrap()
|
||||
});
|
||||
static IGNORE_CLASS: &[&str] = &["menu-title"];
|
||||
static IGNORE_CLASS: &[&str] = &["menu-title", "mdbook-help-title"];
|
||||
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
@@ -696,7 +711,7 @@ fn build_header_links(html: &str) -> String {
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Insert a sinle link into a header, making sure each link gets its own
|
||||
/// Insert a single link into a header, making sure each link gets its own
|
||||
/// unique ID by appending an auto-incremented number (if necessary).
|
||||
fn insert_link_into_header(
|
||||
level: usize,
|
||||
@@ -724,8 +739,8 @@ fn insert_link_into_header(
|
||||
// ```
|
||||
// This function replaces all commas by spaces in the code block classes
|
||||
fn fix_code_blocks(html: &str) -> String {
|
||||
static FIX_CODE_BLOCKS: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap());
|
||||
static FIX_CODE_BLOCKS: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap());
|
||||
|
||||
FIX_CODE_BLOCKS
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
@@ -738,8 +753,8 @@ fn fix_code_blocks(html: &str) -> String {
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
static CODE_BLOCK_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
|
||||
static CODE_BLOCK_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
|
||||
|
||||
fn add_playground_pre(
|
||||
html: &str,
|
||||
@@ -807,8 +822,10 @@ fn add_playground_pre(
|
||||
/// Modifies all `<code>` blocks to convert "hidden" lines and to wrap them in
|
||||
/// a `<span class="boring">`.
|
||||
fn hide_lines(html: &str, code_config: &Code) -> String {
|
||||
static LANGUAGE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\blanguage-(\w+)\b").unwrap());
|
||||
static HIDELINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\bhidelines=(\S+)").unwrap());
|
||||
static LANGUAGE_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\blanguage-(\w+)\b").unwrap());
|
||||
static HIDELINES_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\bhidelines=(\S+)").unwrap());
|
||||
|
||||
CODE_BLOCK_RE
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
@@ -849,7 +866,8 @@ fn hide_lines(html: &str, code_config: &Code) -> String {
|
||||
}
|
||||
|
||||
fn hide_lines_rust(content: &str) -> String {
|
||||
static BORING_LINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap());
|
||||
static BORING_LINES_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap());
|
||||
|
||||
let mut result = String::with_capacity(content.len());
|
||||
let mut lines = content.lines().peekable();
|
||||
@@ -930,6 +948,62 @@ struct RenderItemContext<'a> {
|
||||
chapter_titles: &'a HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
/// Redirect mapping.
|
||||
///
|
||||
/// The key is the source path (like `foo/bar.html`). The value is a tuple
|
||||
/// `(destination_path, fragment_map)`. The `destination_path` is the page to
|
||||
/// redirect to. `fragment_map` is the map of fragments that override the
|
||||
/// destination. For example, a fragment `#foo` could redirect to any other
|
||||
/// page or site.
|
||||
type CombinedRedirects = BTreeMap<String, (String, BTreeMap<String, String>)>;
|
||||
fn combine_fragment_redirects(redirects: &HashMap<String, String>) -> CombinedRedirects {
|
||||
let mut combined: CombinedRedirects = BTreeMap::new();
|
||||
// This needs to extract the fragments to generate the fragment map.
|
||||
for (original, new) in redirects {
|
||||
if let Some((source_path, source_fragment)) = original.rsplit_once('#') {
|
||||
let e = combined.entry(source_path.to_string()).or_default();
|
||||
if let Some(old) = e.1.insert(format!("#{source_fragment}"), new.clone()) {
|
||||
log::error!(
|
||||
"internal error: found duplicate fragment redirect \
|
||||
{old} for {source_path}#{source_fragment}"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let e = combined.entry(original.to_string()).or_default();
|
||||
e.0 = new.clone();
|
||||
}
|
||||
}
|
||||
combined
|
||||
}
|
||||
|
||||
/// Collects fragment redirects for an existing page.
|
||||
///
|
||||
/// The returned map has keys like `#foo` and the value is the new destination
|
||||
/// path or URL.
|
||||
fn collect_redirects_for_path(
|
||||
path: &Path,
|
||||
redirects: &HashMap<String, String>,
|
||||
) -> Result<BTreeMap<String, String>> {
|
||||
let path = format!("/{}", path.display().to_string().replace('\\', "/"));
|
||||
if redirects.contains_key(&path) {
|
||||
bail!(
|
||||
"redirect found for existing chapter at `{path}`\n\
|
||||
Either delete the redirect or remove the chapter."
|
||||
);
|
||||
}
|
||||
|
||||
let key_prefix = format!("{path}#");
|
||||
let map = redirects
|
||||
.iter()
|
||||
.filter_map(|(source, dest)| {
|
||||
source
|
||||
.strip_prefix(&key_prefix)
|
||||
.map(|fragment| (format!("#{fragment}"), dest.to_string()))
|
||||
})
|
||||
.collect();
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::TextDirection;
|
||||
|
||||
@@ -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};
|
||||
@@ -66,7 +66,13 @@ pub fn create_files(
|
||||
if search_config.copy_js {
|
||||
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 = Object.assign(window.search, JSON.parse('{}'));",
|
||||
index.replace("\\", "\\\\").replace("'", "\\'")
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
static_files.add_builtin("searcher.js", searcher::JS);
|
||||
static_files.add_builtin("mark.min.js", searcher::MARK_JS);
|
||||
@@ -308,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");
|
||||
@@ -403,3 +409,92 @@ fn chapter_settings_priority() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_basic() {
|
||||
assert_eq!(tokenize("hello world"), vec!["hello", "world"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_with_hyphens() {
|
||||
assert_eq!(
|
||||
tokenize("hello-world test-case"),
|
||||
vec!["hello", "world", "test", "case"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_mixed_whitespace() {
|
||||
assert_eq!(
|
||||
tokenize("hello\tworld\ntest\r\ncase"),
|
||||
vec!["hello", "world", "test", "case"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_empty_string() {
|
||||
assert_eq!(tokenize(""), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_only_whitespace() {
|
||||
assert_eq!(tokenize(" \t\n "), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_case_normalization() {
|
||||
assert_eq!(tokenize("Hello WORLD Test"), vec!["hello", "world", "test"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_trim_whitespace() {
|
||||
assert_eq!(tokenize(" hello world "), vec!["hello", "world"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_long_words_filtered() {
|
||||
let long_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX + 1);
|
||||
let short_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX);
|
||||
let input = format!("{} hello {}", long_word, short_word);
|
||||
assert_eq!(tokenize(&input), vec!["hello", &short_word]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_max_length_word() {
|
||||
let max_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX);
|
||||
assert_eq!(tokenize(&max_word), vec![max_word]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_special_characters() {
|
||||
assert_eq!(
|
||||
tokenize("hello,world.test!case?"),
|
||||
vec!["hello,world.test!case?"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_unicode() {
|
||||
assert_eq!(
|
||||
tokenize("café naïve résumé"),
|
||||
vec!["café", "naïve", "résumé"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_unicode_rtl_hebre() {
|
||||
assert_eq!(tokenize("שלום עולם"), vec!["שלום", "עולם"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tokenize_numbers() {
|
||||
assert_eq!(
|
||||
tokenize("test123 456-789 hello"),
|
||||
vec!["test123", "456", "789", "hello"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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,6 +209,11 @@ 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,
|
||||
@@ -229,10 +236,10 @@ pub fn render_markdown_with_path(
|
||||
// `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 list of (name, Vec<Event>)
|
||||
// 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 = Vec::new();
|
||||
let mut footnote_defs = HashMap::new();
|
||||
|
||||
// The following are used when currently processing a footnote definition.
|
||||
//
|
||||
@@ -262,7 +269,16 @@ pub fn render_markdown_with_path(
|
||||
Event::End(TagEnd::FootnoteDefinition) => {
|
||||
let def_events = std::mem::take(&mut in_footnote);
|
||||
let name = std::mem::take(&mut in_footnote_name);
|
||||
footnote_defs.push((name, def_events));
|
||||
|
||||
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) => {
|
||||
@@ -298,7 +314,12 @@ pub fn render_markdown_with_path(
|
||||
html::push_html(&mut body, events);
|
||||
|
||||
if !footnote_defs.is_empty() {
|
||||
add_footnote_defs(&mut body, path, footnote_defs, &footnote_numbers);
|
||||
add_footnote_defs(
|
||||
&mut body,
|
||||
path,
|
||||
footnote_defs.into_iter().collect(),
|
||||
&footnote_numbers,
|
||||
);
|
||||
}
|
||||
|
||||
body
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -25,4 +25,27 @@ expand = true
|
||||
heading-split-level = 2
|
||||
|
||||
[output.html.redirect]
|
||||
"/format/config.html" = "configuration/index.html"
|
||||
"/format/config.html" = "../prefix.html"
|
||||
|
||||
# This is a source without a fragment, and one with a fragment that goes to
|
||||
# the same place. The redirect with the fragment is not necessary, since that
|
||||
# is the default behavior.
|
||||
"/pointless-fragment.html" = "prefix.html"
|
||||
"/pointless-fragment.html#foo" = "prefix.html#foo"
|
||||
|
||||
"/rename-page-and-fragment.html" = "prefix.html"
|
||||
"/rename-page-and-fragment.html#orig" = "prefix.html#new"
|
||||
|
||||
"/rename-page-fragment-elsewhere.html" = "prefix.html"
|
||||
"/rename-page-fragment-elsewhere.html#orig" = "suffix.html#new"
|
||||
|
||||
# Rename fragment on an existing page.
|
||||
"/prefix.html#orig" = "prefix.html#new"
|
||||
# Rename fragment on an existing page to another page.
|
||||
"/prefix.html#orig-new-page" = "suffix.html#new"
|
||||
|
||||
"/full-url-with-fragment.html" = "https://www.rust-lang.org/#fragment"
|
||||
|
||||
"/full-url-with-fragment-map.html" = "https://www.rust-lang.org/"
|
||||
"/full-url-with-fragment-map.html#a" = "https://www.rust-lang.org/#new1"
|
||||
"/full-url-with-fragment-map.html#b" = "https://www.rust-lang.org/#new2"
|
||||
|
||||
@@ -17,7 +17,7 @@ This line contains `inline code` mixed with some other stuff. (LTR)
|
||||
---
|
||||
|
||||
````
|
||||
escaping ``` in ```, fun, isn't is?
|
||||
escaping ``` in ```, fun, isn't it?
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
@@ -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,46 +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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_cli() {
|
||||
let mut cmd = mdbook_cmd();
|
||||
cmd.assert()
|
||||
.failure()
|
||||
.code(2)
|
||||
.stdout(predicates::str::is_empty())
|
||||
.stderr(predicates::str::contains(
|
||||
"Creates a book from markdown files",
|
||||
));
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
mod cli;
|
||||
mod dummy_book;
|
||||
@@ -1,68 +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());
|
||||
let error_message = got.err().unwrap().to_string();
|
||||
let status = if cfg!(windows) {
|
||||
"exit code: 1"
|
||||
} else {
|
||||
"exit status: 1"
|
||||
};
|
||||
assert_eq!(
|
||||
error_message,
|
||||
format!(
|
||||
r#"The "nop-preprocessor" preprocessor exited unsuccessfully with {status} status"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[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,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
|
||||
@@ -1 +0,0 @@
|
||||
assert!($TEST_STATUS);
|
||||
@@ -1,31 +0,0 @@
|
||||
# Nested Chapter
|
||||
|
||||
This file has some testable code.
|
||||
|
||||
```rust
|
||||
assert!($TEST_STATUS);
|
||||
```
|
||||
|
||||
## Some Section
|
||||
|
||||
```rust
|
||||
{{#include nested-test.rs}}
|
||||
```
|
||||
|
||||
## Anchors include the part of a file between special comments
|
||||
|
||||
```rust
|
||||
{{#include nested-test-with-anchors.rs:myanchor}}
|
||||
```
|
||||
|
||||
## Rustdoc include adds the rest of the file as hidden
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include partially-included-test.rs:5:7}}
|
||||
```
|
||||
|
||||
## Rustdoc include works with anchors too
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include partially-included-test-with-anchors.rs:rustdoc-include-anchor}}
|
||||
```
|
||||
@@ -1,3 +0,0 @@
|
||||
# Introduction
|
||||
|
||||
Here's some interesting text...
|
||||
@@ -1,5 +0,0 @@
|
||||
# Second Chapter
|
||||
|
||||
This makes sure you can insert runnable Rust files.
|
||||
|
||||
{{#playground example.rs}}
|
||||
@@ -1 +0,0 @@
|
||||
# Root README
|
||||
@@ -1,7 +0,0 @@
|
||||
# This dummy book is for testing the conversion of README.md to index.html by IndexPreprocessor
|
||||
|
||||
[Root README](README.md)
|
||||
|
||||
- [1st README](first/README.md)
|
||||
- [2nd README](second/README.md)
|
||||
- [2nd index](second/index.md)
|
||||
@@ -1 +0,0 @@
|
||||
# First README
|
||||
@@ -1 +0,0 @@
|
||||
# Second README
|
||||
@@ -1 +0,0 @@
|
||||
# Second index
|
||||
16
tests/gui/help.goml
Normal file
16
tests/gui/help.goml
Normal file
@@ -0,0 +1,16 @@
|
||||
// This GUI test checks help popup.
|
||||
|
||||
go-to: |DOC_PATH| + "index.html"
|
||||
assert-css: ("#mdbook-help-container", {"display": "none"})
|
||||
press-key: '?'
|
||||
wait-for-css: ("#mdbook-help-container", {"display": "flex"})
|
||||
press-key: 'Escape'
|
||||
wait-for-css: ("#mdbook-help-container", {"display": "none"})
|
||||
press-key: '?'
|
||||
wait-for-css: ("#mdbook-help-container", {"display": "flex"})
|
||||
// Click inside does nothing.
|
||||
click: "#mdbook-help-popup"
|
||||
wait-for-css: ("#mdbook-help-container", {"display": "flex"})
|
||||
// Click outside dismisses.
|
||||
click: "*"
|
||||
wait-for-css: ("#mdbook-help-container", {"display": "none"})
|
||||
@@ -1,8 +1,5 @@
|
||||
// This tests pressing the left and right arrows moving to previous and next page.
|
||||
|
||||
// We disable the requests checks because `mode-rust.js` is not found.
|
||||
fail-on-request-error: false
|
||||
|
||||
go-to: |DOC_PATH| + "index.html"
|
||||
|
||||
// default page is the first numbered page
|
||||
|
||||
51
tests/gui/redirect.goml
Normal file
51
tests/gui/redirect.goml
Normal file
@@ -0,0 +1,51 @@
|
||||
go-to: |DOC_PATH| + "format/config.html"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
|
||||
|
||||
// Check that it preserves fragments when redirecting.
|
||||
go-to: |DOC_PATH| + "format/config.html#fragment"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#fragment"})
|
||||
|
||||
// The fragment one here isn't necessary, but should still work.
|
||||
go-to: |DOC_PATH| + "pointless-fragment.html"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
|
||||
go-to: |DOC_PATH| + "pointless-fragment.html#foo"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#foo"})
|
||||
|
||||
// Page rename, and a fragment rename.
|
||||
go-to: |DOC_PATH| + "rename-page-and-fragment.html"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
|
||||
go-to: |DOC_PATH| + "rename-page-and-fragment.html#orig"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#new"})
|
||||
|
||||
// Page rename, and the fragment goes to a *different* page from the default.
|
||||
go-to: |DOC_PATH| + "rename-page-fragment-elsewhere.html"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
|
||||
go-to: |DOC_PATH| + "rename-page-fragment-elsewhere.html#orig"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "suffix.html#new"})
|
||||
|
||||
// Goes to an external site.
|
||||
go-to: |DOC_PATH| + "full-url-with-fragment.html"
|
||||
assert-window-property: ({"location": "https://www.rust-lang.org/#fragment"})
|
||||
|
||||
// External site with fragment renames.
|
||||
go-to: |DOC_PATH| + "full-url-with-fragment-map.html#a"
|
||||
assert-window-property: ({"location": "https://www.rust-lang.org/#new1"})
|
||||
go-to: |DOC_PATH| + "full-url-with-fragment-map.html#b"
|
||||
assert-window-property: ({"location": "https://www.rust-lang.org/#new2"})
|
||||
|
||||
// Rename fragment on an existing page.
|
||||
go-to: |DOC_PATH| + "prefix.html#orig"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#new"})
|
||||
|
||||
// Other fragments aren't affected.
|
||||
go-to: |DOC_PATH| + "index.html" // Reset page since redirects are processed on load.
|
||||
go-to: |DOC_PATH| + "prefix.html"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
|
||||
go-to: |DOC_PATH| + "index.html" // Reset page since redirects are processed on load.
|
||||
go-to: |DOC_PATH| + "prefix.html#dont-change"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#dont-change"})
|
||||
|
||||
// Rename fragment on an existing page to another page.
|
||||
go-to: |DOC_PATH| + "index.html" // Reset page since redirects are processed on load.
|
||||
go-to: |DOC_PATH| + "prefix.html#orig-new-page"
|
||||
assert-window-property: ({"location": |DOC_PATH| + "suffix.html#new"})
|
||||
@@ -1,5 +1,6 @@
|
||||
// This tests basic search behavior.
|
||||
|
||||
fail-on-js-error: true
|
||||
go-to: |DOC_PATH| + "index.html"
|
||||
|
||||
define-function: (
|
||||
@@ -7,7 +8,7 @@ define-function: (
|
||||
[],
|
||||
block {
|
||||
assert-css: ("#search-wrapper", {"display": "none"})
|
||||
press-key: 'S'
|
||||
press-key: 's'
|
||||
wait-for-css-false: ("#search-wrapper", {"display": "none"})
|
||||
}
|
||||
)
|
||||
@@ -28,3 +29,48 @@ assert-text: ("#searchresults-header", "")
|
||||
call-function: ("open-search", {})
|
||||
write: "strikethrough"
|
||||
wait-for-text: ("#searchresults-header", "2 search results for 'strikethrough':")
|
||||
|
||||
// Now we test search shortcuts and more page changes.
|
||||
go-to: |DOC_PATH| + "index.html"
|
||||
|
||||
// This check is to ensure that the search bar is inside the search wrapper.
|
||||
assert: "#search-wrapper #searchbar"
|
||||
assert-css: ("#search-wrapper", {"display": "none"})
|
||||
|
||||
// Now we make sure the search input appear with the `S` shortcut.
|
||||
press-key: 's'
|
||||
wait-for-css-false: ("#search-wrapper", {"display": "none"})
|
||||
// We ensure the search bar has the focus.
|
||||
assert: "#searchbar:focus"
|
||||
// Pressing a key will therefore update the search input.
|
||||
press-key: 't'
|
||||
assert-text: ("#searchbar", "t")
|
||||
|
||||
// Now we press `Escape` to ensure that the search input disappears again.
|
||||
press-key: 'Escape'
|
||||
wait-for-css: ("#search-wrapper", {"display": "none"})
|
||||
|
||||
// Making it appear by clicking on the search button.
|
||||
click: "#search-toggle"
|
||||
wait-for-css: ("#search-wrapper", {"display": "block"})
|
||||
// We ensure the search bar has the focus.
|
||||
assert: "#searchbar:focus"
|
||||
|
||||
// We input "test".
|
||||
write: "test"
|
||||
// The results should now appear.
|
||||
wait-for-text: ("#searchresults-header", "search results for 'test':", ENDS_WITH)
|
||||
assert: "#searchresults"
|
||||
// Ensure that the URL was updated as well.
|
||||
assert-document-property: ({"URL": "?search=test"}, ENDS_WITH)
|
||||
|
||||
// Now we ensure that when we land on the page with a "search in progress", the search results are
|
||||
// loaded and that the search input has focus.
|
||||
go-to: |DOC_PATH| + "index.html?search=test"
|
||||
wait-for-text: ("#searchresults-header", "search results for 'test':", ENDS_WITH)
|
||||
assert: "#searchbar:focus"
|
||||
assert: "#searchresults"
|
||||
|
||||
// And now we press `Escape` to close everything.
|
||||
press-key: 'Escape'
|
||||
wait-for-css: ("#search-wrapper", {"display": "none"})
|
||||
|
||||
17
tests/gui/sidebar-active.goml
Normal file
17
tests/gui/sidebar-active.goml
Normal file
@@ -0,0 +1,17 @@
|
||||
// This GUI test checks the active page sidebar highlight.
|
||||
|
||||
go-to: |DOC_PATH| + "index.html"
|
||||
|
||||
assert-text: ("mdbook-sidebar-scrollbox a.active", "Prefix Chapter")
|
||||
|
||||
go-to: |DOC_PATH| + "individual/index.html"
|
||||
|
||||
assert-text: ("mdbook-sidebar-scrollbox a.active", "3. Markdown Individual tags")
|
||||
|
||||
go-to: |DOC_PATH| + "index.html?highlight=test"
|
||||
|
||||
assert-text: ("mdbook-sidebar-scrollbox a.active", "Prefix Chapter")
|
||||
|
||||
go-to: |DOC_PATH| + "individual/index.html?highlight=test"
|
||||
|
||||
assert-text: ("mdbook-sidebar-scrollbox a.active", "3. Markdown Individual tags")
|
||||
@@ -7,23 +7,20 @@ set-window-size: (1100, 600)
|
||||
reload:
|
||||
|
||||
store-value: (content_indent, 308)
|
||||
store-value: (sidebar_storage_value, "mdbook-sidebar")
|
||||
store-value: (sidebar_storage_hidden_value, "hidden")
|
||||
store-value: (sidebar_storage_displayed_value, "visible")
|
||||
|
||||
define-function: (
|
||||
"hide-sidebar",
|
||||
[],
|
||||
block {
|
||||
// The content should be "moved" to the right because of the sidebar.
|
||||
assert-css: ("#sidebar", {"transform": "none"})
|
||||
assert-position: ("#page-wrapper", {"x": |content_indent|})
|
||||
|
||||
// We now hide the sidebar.
|
||||
click: "#sidebar-toggle"
|
||||
wait-for: "body.sidebar-hidden"
|
||||
// `transform` is 0.3s so we need to wait a bit (0.5s) to ensure the animation is done.
|
||||
wait-for: 5000
|
||||
assert-css-false: ("#sidebar", {"transform": "none"})
|
||||
// The page content should now be on the left.
|
||||
assert-position: ("#page-wrapper", {"x": 0})
|
||||
wait-for-css: ("#sidebar", {"display": "none"})
|
||||
assert-local-storage: {|sidebar_storage_value|: |sidebar_storage_hidden_value|}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -31,26 +28,42 @@ define-function: (
|
||||
"show-sidebar",
|
||||
[],
|
||||
block {
|
||||
// The page content should be on the left and the sidebar "moved out".
|
||||
assert-css: ("#sidebar", {"transform": "matrix(1, 0, 0, 1, -308, 0)"})
|
||||
assert-css: ("#sidebar", {"display": "none"})
|
||||
assert-position: ("#page-wrapper", {"x": 0})
|
||||
|
||||
// We expand the sidebar.
|
||||
click: "#sidebar-toggle"
|
||||
wait-for: "body.sidebar-visible"
|
||||
wait-for-css-false: ("#sidebar", {"display": "none"})
|
||||
// `transform` is 0.3s so we need to wait a bit (0.5s) to ensure the animation is done.
|
||||
wait-for: 5000
|
||||
assert-css-false: ("#sidebar", {"transform": "matrix(1, 0, 0, 1, -308, 0)"})
|
||||
// The page content should be moved to the right.
|
||||
assert-position: ("#page-wrapper", {"x": |content_indent|})
|
||||
assert-local-storage: {|sidebar_storage_value|: |sidebar_storage_displayed_value|}
|
||||
},
|
||||
)
|
||||
|
||||
// Since the sidebar is visible, we should be able to find this text.
|
||||
assert-find-text: ("3.9. Links and Horizontal Rule", {"case-sensitive": true})
|
||||
call-function: ("hide-sidebar", {})
|
||||
// Text should not be findeable anymore since the sidebar is collapsed.
|
||||
assert-find-text-false: ("3.9. Links and Horizontal Rule", {"case-sensitive": true})
|
||||
call-function: ("show-sidebar", {})
|
||||
// We should be able to find this text again.
|
||||
assert-find-text: ("3.9. Links and Horizontal Rule", {"case-sensitive": true})
|
||||
|
||||
// We now test on smaller width to ensure that the sidebar is collapsed by default.
|
||||
set-window-size: (900, 600)
|
||||
reload:
|
||||
call-function: ("show-sidebar", {})
|
||||
call-function: ("hide-sidebar", {})
|
||||
|
||||
// We now test that if the sidebar is considered open and we reload the page, since
|
||||
// the width is small, it will still be collapsed.
|
||||
set-local-storage: {|sidebar_storage_value|: |sidebar_storage_displayed_value|}
|
||||
reload:
|
||||
// The stored value shouldn't have changed.
|
||||
assert-local-storage: {|sidebar_storage_value|: |sidebar_storage_displayed_value|}
|
||||
// But the sidebar should be hidden anyway.
|
||||
assert-css: ("#sidebar", {"display": "none"})
|
||||
assert-position: ("#page-wrapper", {"x": 0})
|
||||
|
||||
157
tests/init.rs
157
tests/init.rs
@@ -1,157 +0,0 @@
|
||||
use mdbook::config::Config;
|
||||
use mdbook::MDBook;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
|
||||
/// Run `mdbook init` in an empty directory and make sure the default files
|
||||
/// are created.
|
||||
#[test]
|
||||
fn base_mdbook_init_should_create_default_content() {
|
||||
let created_files = vec!["book", "src", "src/SUMMARY.md", "src/chapter_1.md"];
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
for file in &created_files {
|
||||
assert!(!temp.path().join(file).exists());
|
||||
}
|
||||
|
||||
MDBook::init(temp.path()).build().unwrap();
|
||||
|
||||
for file in &created_files {
|
||||
let target = temp.path().join(file);
|
||||
println!("{}", target.display());
|
||||
assert!(target.exists(), "{file} doesn't exist");
|
||||
}
|
||||
|
||||
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
|
||||
assert_eq!(
|
||||
contents,
|
||||
"[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"src\"\n"
|
||||
);
|
||||
}
|
||||
|
||||
/// Run `mdbook init` in a directory containing a SUMMARY.md should create the
|
||||
/// files listed in the summary.
|
||||
#[test]
|
||||
fn run_mdbook_init_should_create_content_from_summary() {
|
||||
let created_files = vec!["intro.md", "first.md", "outro.md"];
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
let src_dir = temp.path().join("src");
|
||||
fs::create_dir_all(src_dir.clone()).unwrap();
|
||||
static SUMMARY: &str = r#"# Summary
|
||||
|
||||
[intro](intro.md)
|
||||
|
||||
- [First chapter](first.md)
|
||||
|
||||
[outro](outro.md)
|
||||
|
||||
"#;
|
||||
|
||||
let mut summary = File::create(src_dir.join("SUMMARY.md")).unwrap();
|
||||
summary.write_all(SUMMARY.as_bytes()).unwrap();
|
||||
MDBook::init(temp.path()).build().unwrap();
|
||||
|
||||
for file in &created_files {
|
||||
let target = src_dir.join(file);
|
||||
println!("{}", target.display());
|
||||
assert!(target.exists(), "{file} doesn't exist");
|
||||
}
|
||||
}
|
||||
|
||||
/// Set some custom arguments for where to place the source and destination
|
||||
/// files, then call `mdbook init`.
|
||||
#[test]
|
||||
fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||
let created_files = vec!["out", "in", "in/SUMMARY.md", "in/chapter_1.md"];
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
for file in &created_files {
|
||||
assert!(
|
||||
!temp.path().join(file).exists(),
|
||||
"{file} shouldn't exist yet!"
|
||||
);
|
||||
}
|
||||
|
||||
let mut cfg = Config::default();
|
||||
cfg.book.src = PathBuf::from("in");
|
||||
cfg.build.build_dir = PathBuf::from("out");
|
||||
|
||||
MDBook::init(temp.path()).with_config(cfg).build().unwrap();
|
||||
|
||||
for file in &created_files {
|
||||
let target = temp.path().join(file);
|
||||
assert!(
|
||||
target.exists(),
|
||||
"{file} should have been created by `mdbook init`"
|
||||
);
|
||||
}
|
||||
|
||||
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
|
||||
assert_eq!(
|
||||
contents,
|
||||
"[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"in\"\n\n[build]\nbuild-dir = \"out\"\ncreate-missing = true\nextra-watch-dirs = []\nuse-default-preprocessors = true\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn book_toml_isnt_required() {
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
let md = MDBook::init(temp.path()).build().unwrap();
|
||||
|
||||
let _ = fs::remove_file(temp.path().join("book.toml"));
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_theme() {
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
MDBook::init(temp.path()).copy_theme(true).build().unwrap();
|
||||
let expected = vec![
|
||||
"book.js",
|
||||
"css/chrome.css",
|
||||
"css/general.css",
|
||||
"css/print.css",
|
||||
"css/variables.css",
|
||||
"favicon.png",
|
||||
"favicon.svg",
|
||||
"fonts/OPEN-SANS-LICENSE.txt",
|
||||
"fonts/SOURCE-CODE-PRO-LICENSE.txt",
|
||||
"fonts/fonts.css",
|
||||
"fonts/open-sans-v17-all-charsets-300.woff2",
|
||||
"fonts/open-sans-v17-all-charsets-300italic.woff2",
|
||||
"fonts/open-sans-v17-all-charsets-600.woff2",
|
||||
"fonts/open-sans-v17-all-charsets-600italic.woff2",
|
||||
"fonts/open-sans-v17-all-charsets-700.woff2",
|
||||
"fonts/open-sans-v17-all-charsets-700italic.woff2",
|
||||
"fonts/open-sans-v17-all-charsets-800.woff2",
|
||||
"fonts/open-sans-v17-all-charsets-800italic.woff2",
|
||||
"fonts/open-sans-v17-all-charsets-italic.woff2",
|
||||
"fonts/open-sans-v17-all-charsets-regular.woff2",
|
||||
"fonts/source-code-pro-v11-all-charsets-500.woff2",
|
||||
"highlight.css",
|
||||
"highlight.js",
|
||||
"index.hbs",
|
||||
];
|
||||
let theme_dir = temp.path().join("theme");
|
||||
let mut actual: Vec<_> = walkdir::WalkDir::new(&theme_dir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| !e.file_type().is_dir())
|
||||
.map(|e| {
|
||||
e.path()
|
||||
.strip_prefix(&theme_dir)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace('\\', "/")
|
||||
})
|
||||
.collect();
|
||||
actual.sort();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//! Some integration tests to make sure the `SUMMARY.md` parser can deal with
|
||||
//! some real-life examples.
|
||||
|
||||
use mdbook::book;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
macro_rules! summary_md_test {
|
||||
($name:ident, $filename:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let filename = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("summary_md_files")
|
||||
.join($filename);
|
||||
|
||||
if !filename.exists() {
|
||||
panic!("{} Doesn't exist", filename.display());
|
||||
}
|
||||
|
||||
let mut content = String::new();
|
||||
File::open(&filename)
|
||||
.unwrap()
|
||||
.read_to_string(&mut content)
|
||||
.unwrap();
|
||||
|
||||
if let Err(e) = book::parse_summary(&content) {
|
||||
eprintln!("Error parsing {}", filename.display());
|
||||
eprintln!();
|
||||
eprintln!("{:?}", e);
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
summary_md_test!(rust_by_example, "rust_by_example.md");
|
||||
summary_md_test!(rust_ffi_guide, "rust_ffi_guide.md");
|
||||
summary_md_test!(example_book, "example_book.md");
|
||||
summary_md_test!(the_book_2nd_edition, "the_book-2nd_edition.md");
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
# Summary
|
||||
|
||||
- [mdBook](README.md)
|
||||
- [Command Line Tool](cli/cli-tool.md)
|
||||
- [init](cli/init.md)
|
||||
- [build](cli/build.md)
|
||||
- [watch](cli/watch.md)
|
||||
- [serve](cli/serve.md)
|
||||
- [test](cli/test.md)
|
||||
- [Format](format/format.md)
|
||||
- [SUMMARY.md](format/summary.md)
|
||||
- [Configuration](format/config.md)
|
||||
- [Theme](format/theme/theme.md)
|
||||
- [index.hbs](format/theme/index-hbs.md)
|
||||
- [Syntax highlighting](format/theme/syntax-highlighting.md)
|
||||
- [MathJax Support](format/mathjax.md)
|
||||
- [Rust code specific features](format/rust.md)
|
||||
- [Rust Library](lib/lib.md)
|
||||
-----------
|
||||
[Contributors](misc/contributors.md)
|
||||
@@ -1,191 +0,0 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](index.md)
|
||||
|
||||
- [Hello World](hello.md)
|
||||
- [Comments](hello/comment.md)
|
||||
- [Formatted print](hello/print.md)
|
||||
- [Debug](hello/print/print_debug.md)
|
||||
- [Display](hello/print/print_display.md)
|
||||
- [Testcase: List](hello/print/print_display/testcase_list.md)
|
||||
- [Formatting](hello/print/fmt.md)
|
||||
|
||||
- [Primitives](primitives.md)
|
||||
- [Literals and operators](primitives/literals.md)
|
||||
- [Tuples](primitives/tuples.md)
|
||||
- [Arrays and Slices](primitives/array.md)
|
||||
|
||||
- [Custom Types](custom_types.md)
|
||||
- [Structures](custom_types/structs.md)
|
||||
- [Enums](custom_types/enum.md)
|
||||
- [use](custom_types/enum/enum_use.md)
|
||||
- [C-like](custom_types/enum/c_like.md)
|
||||
- [Testcase: linked-list](custom_types/enum/testcase_linked_list.md)
|
||||
- [constants](custom_types/constants.md)
|
||||
|
||||
- [Variable Bindings](variable_bindings.md)
|
||||
- [Mutability](variable_bindings/mut.md)
|
||||
- [Scope and Shadowing](variable_bindings/scope.md)
|
||||
- [Declare first](variable_bindings/declare.md)
|
||||
|
||||
- [Types](types.md)
|
||||
- [Casting](types/cast.md)
|
||||
- [Literals](types/literals.md)
|
||||
- [Inference](types/inference.md)
|
||||
- [Aliasing](types/alias.md)
|
||||
|
||||
- [Conversion](conversion.md)
|
||||
- [From and Into](conversion/from_into.md)
|
||||
- [To and From String](conversion/string.md)
|
||||
|
||||
- [Expressions](expression.md)
|
||||
|
||||
- [Flow Control](flow_control.md)
|
||||
- [if/else](flow_control/if_else.md)
|
||||
- [loop](flow_control/loop.md)
|
||||
- [Nesting and labels](flow_control/loop/nested.md)
|
||||
- [Returning from loops](flow_control/loop/return.md)
|
||||
- [while](flow_control/while.md)
|
||||
- [for and range](flow_control/for.md)
|
||||
- [match](flow_control/match.md)
|
||||
- [Destructuring](flow_control/match/destructuring.md)
|
||||
- [tuples](flow_control/match/destructuring/destructure_tuple.md)
|
||||
- [enums](flow_control/match/destructuring/destructure_enum.md)
|
||||
- [pointers/ref](flow_control/match/destructuring/destructure_pointers.md)
|
||||
- [structs](flow_control/match/destructuring/destructure_structures.md)
|
||||
- [Guards](flow_control/match/guard.md)
|
||||
- [Binding](flow_control/match/binding.md)
|
||||
- [if let](flow_control/if_let.md)
|
||||
- [while let](flow_control/while_let.md)
|
||||
|
||||
- [Functions](fn.md)
|
||||
- [Methods](fn/methods.md)
|
||||
- [Closures](fn/closures.md)
|
||||
- [Capturing](fn/closures/capture.md)
|
||||
- [As input parameters](fn/closures/input_parameters.md)
|
||||
- [Type anonymity](fn/closures/anonymity.md)
|
||||
- [Input functions](fn/closures/input_functions.md)
|
||||
- [As output parameters](fn/closures/output_parameters.md)
|
||||
- [Examples in `std`](fn/closures/closure_examples.md)
|
||||
- [Iterator::any](fn/closures/closure_examples/iter_any.md)
|
||||
- [Iterator::find](fn/closures/closure_examples/iter_find.md)
|
||||
- [Higher Order Functions](fn/hof.md)
|
||||
|
||||
- [Modules](mod.md)
|
||||
- [Visibility](mod/visibility.md)
|
||||
- [Struct visibility](mod/struct_visibility.md)
|
||||
- [The `use` declaration](mod/use.md)
|
||||
- [`super` and `self`](mod/super.md)
|
||||
- [File hierarchy](mod/split.md)
|
||||
|
||||
- [Crates](crates.md)
|
||||
- [Library](crates/lib.md)
|
||||
- [`extern crate`](crates/link.md)
|
||||
|
||||
- [Attributes](attribute.md)
|
||||
- [`dead_code`](attribute/unused.md)
|
||||
- [Crates](attribute/crate.md)
|
||||
- [`cfg`](attribute/cfg.md)
|
||||
- [Custom](attribute/cfg/custom.md)
|
||||
|
||||
- [Generics](generics.md)
|
||||
- [Functions](generics/gen_fn.md)
|
||||
- [Implementation](generics/impl.md)
|
||||
- [Traits](generics/gen_trait.md)
|
||||
- [Bounds](generics/bounds.md)
|
||||
- [Testcase: empty bounds](generics/bounds/testcase_empty.md)
|
||||
- [Multiple bounds](generics/multi_bounds.md)
|
||||
- [Where clauses](generics/where.md)
|
||||
- [New Type Idiom](generics/new_types.md)
|
||||
- [Associated items](generics/assoc_items.md)
|
||||
- [The Problem](generics/assoc_items/the_problem.md)
|
||||
- [Associated types](generics/assoc_items/types.md)
|
||||
- [Phantom type parameters](generics/phantom.md)
|
||||
- [Testcase: unit clarification](generics/phantom/testcase_units.md)
|
||||
|
||||
- [Scoping rules](scope.md)
|
||||
- [RAII](scope/raii.md)
|
||||
- [Ownership and moves](scope/move.md)
|
||||
- [Mutability](scope/move/mut.md)
|
||||
- [Borrowing](scope/borrow.md)
|
||||
- [Mutability](scope/borrow/mut.md)
|
||||
- [Freezing](scope/borrow/freeze.md)
|
||||
- [Aliasing](scope/borrow/alias.md)
|
||||
- [The ref pattern](scope/borrow/ref.md)
|
||||
- [Lifetimes](scope/lifetime.md)
|
||||
- [Explicit annotation](scope/lifetime/explicit.md)
|
||||
- [Functions](scope/lifetime/fn.md)
|
||||
- [Methods](scope/lifetime/methods.md)
|
||||
- [Structs](scope/lifetime/struct.md)
|
||||
- [Bounds](scope/lifetime/lifetime_bounds.md)
|
||||
- [Coercion](scope/lifetime/lifetime_coercion.md)
|
||||
- [static](scope/lifetime/static_lifetime.md)
|
||||
- [elision](scope/lifetime/elision.md)
|
||||
|
||||
- [Traits](trait.md)
|
||||
- [Derive](trait/derive.md)
|
||||
- [Operator Overloading](trait/ops.md)
|
||||
- [Drop](trait/drop.md)
|
||||
- [Iterators](trait/iter.md)
|
||||
- [Clone](trait/clone.md)
|
||||
|
||||
- [macro_rules!](macros.md)
|
||||
- [Syntax](macro/syntax.md)
|
||||
- [Designators](macros/designators.md)
|
||||
- [Overload](macros/overload.md)
|
||||
- [Repeat](macros/repeat.md)
|
||||
- [DRY (Don't Repeat Yourself)](macros/dry.md)
|
||||
- [DSL (Domain Specific Languages)](macros/dsl.md)
|
||||
- [Variadics](macros/variadics.md)
|
||||
|
||||
- [Error handling](error.md)
|
||||
- [`panic`](error/panic.md)
|
||||
- [`Option` & `unwrap`](error/option_unwrap.md)
|
||||
- [Combinators: `map`](error/option_unwrap/map.md)
|
||||
- [Combinators: `and_then`](error/option_unwrap/and_then.md)
|
||||
- [`Result`](error/result.md)
|
||||
- [`map` for `Result`](error/result/result_map.md)
|
||||
- [aliases for `Result`](error/result/result_alias.md)
|
||||
- [Early returns](error/result/early_returns.md)
|
||||
- [Introducing `?`](error/result/enter_question_mark.md)
|
||||
- [Multiple error types](error/multiple_error_types.md)
|
||||
- [Pulling `Result`s out of `Option`s](error/multiple_error_types/option_result.md)
|
||||
- [Defining an error type](error/multiple_error_types/define_error_type.md)
|
||||
- [`Box`ing errors](error/multiple_error_types/boxing_errors.md)
|
||||
- [Other uses of `?`](error/multiple_error_types/reenter_question_mark.md)
|
||||
- [Wrapping errors](error/multiple_error_types/wrap_error.md)
|
||||
- [Iterating over `Result`s](error/iter_result.md)
|
||||
|
||||
- [Std library types](std.md)
|
||||
- [Box, stack and heap](std/box.md)
|
||||
- [Vectors](std/vec.md)
|
||||
- [Strings](std/str.md)
|
||||
- [`Option`](std/option.md)
|
||||
- [`Result`](std/result.md)
|
||||
- [`?`](std/result/question_mark.md)
|
||||
- [`panic!`](std/panic.md)
|
||||
- [HashMap](std/hash.md)
|
||||
- [Alternate/custom key types](std/hash/alt_key_types.md)
|
||||
- [HashSet](std/hash/hashset.md)
|
||||
|
||||
- [Std misc](std_misc.md)
|
||||
- [Threads](std_misc/threads.md)
|
||||
- [Testcase: map-reduce](std_misc/threads/testcase_mapreduce.md)
|
||||
- [Channels](std_misc/channels.md)
|
||||
- [Path](std_misc/path.md)
|
||||
- [File I/O](std_misc/file.md)
|
||||
- [`open`](std_misc/file/open.md)
|
||||
- [`create`](std_misc/file/create.md)
|
||||
- [Child processes](std_misc/process.md)
|
||||
- [Pipes](std_misc/process/pipe.md)
|
||||
- [Wait](std_misc/process/wait.md)
|
||||
- [Filesystem Operations](std_misc/fs.md)
|
||||
- [Program arguments](std_misc/arg.md)
|
||||
- [Argument parsing](std_misc/arg/matching.md)
|
||||
- [Foreign Function Interface](std_misc/ffi.md)
|
||||
|
||||
- [Meta](meta.md)
|
||||
- [Documentation](meta/doc.md)
|
||||
- [Testing](meta/test.md)
|
||||
|
||||
- [Unsafe Operations](unsafe.md)
|
||||
@@ -1,19 +0,0 @@
|
||||
# Summary
|
||||
|
||||
- [Overview](./overview.md)
|
||||
- [Setting Up](./setting_up.md)
|
||||
- [Core Client Library](./client.md)
|
||||
- [Constructing a Basic Request](./basic_request.md)
|
||||
- [Sending the Request](./send_basic.md)
|
||||
- [Generating a Header File](./cbindgen.md)
|
||||
- [Better Error Handling](./error_handling.md)
|
||||
- [Asynchronous Operations](./async.md)
|
||||
- [More Complex Requests](./complex_request.md)
|
||||
- [Testing](./testing.md)
|
||||
- [Dynamic Loading & Plugins](./dynamic_loading.md)
|
||||
|
||||
---
|
||||
|
||||
- [Break All The Things!!1!](./fun/index.md)
|
||||
- [Problems](./fun/problems.md)
|
||||
- [Solutions](./fun/solutions.md)
|
||||
@@ -1,130 +0,0 @@
|
||||
# The Rust Programming Language
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Introduction](ch01-00-introduction.md)
|
||||
- [Installation](ch01-01-installation.md)
|
||||
- [Hello, World!](ch01-02-hello-world.md)
|
||||
|
||||
- [Guessing Game Tutorial](ch02-00-guessing-game-tutorial.md)
|
||||
|
||||
- [Common Programming Concepts](ch03-00-common-programming-concepts.md)
|
||||
- [Variables and Mutability](ch03-01-variables-and-mutability.md)
|
||||
- [Data Types](ch03-02-data-types.md)
|
||||
- [How Functions Work](ch03-03-how-functions-work.md)
|
||||
- [Comments](ch03-04-comments.md)
|
||||
- [Control Flow](ch03-05-control-flow.md)
|
||||
|
||||
- [Understanding Ownership](ch04-00-understanding-ownership.md)
|
||||
- [What is Ownership?](ch04-01-what-is-ownership.md)
|
||||
- [References & Borrowing](ch04-02-references-and-borrowing.md)
|
||||
- [Slices](ch04-03-slices.md)
|
||||
|
||||
- [Using Structs to Structure Related Data](ch05-00-structs.md)
|
||||
- [Defining and Instantiating Structs](ch05-01-defining-structs.md)
|
||||
- [An Example Program Using Structs](ch05-02-example-structs.md)
|
||||
- [Method Syntax](ch05-03-method-syntax.md)
|
||||
|
||||
- [Enums and Pattern Matching](ch06-00-enums.md)
|
||||
- [Defining an Enum](ch06-01-defining-an-enum.md)
|
||||
- [The `match` Control Flow Operator](ch06-02-match.md)
|
||||
- [Concise Control Flow with `if let`](ch06-03-if-let.md)
|
||||
|
||||
## Basic Rust Literacy
|
||||
|
||||
- [Modules](ch07-00-modules.md)
|
||||
- [`mod` and the Filesystem](ch07-01-mod-and-the-filesystem.md)
|
||||
- [Controlling Visibility with `pub`](ch07-02-controlling-visibility-with-pub.md)
|
||||
- [Referring to Names in Different Modules](ch07-03-importing-names-with-use.md)
|
||||
|
||||
- [Common Collections](ch08-00-common-collections.md)
|
||||
- [Vectors](ch08-01-vectors.md)
|
||||
- [Strings](ch08-02-strings.md)
|
||||
- [Hash Maps](ch08-03-hash-maps.md)
|
||||
|
||||
- [Error Handling](ch09-00-error-handling.md)
|
||||
- [Unrecoverable Errors with `panic!`](ch09-01-unrecoverable-errors-with-panic.md)
|
||||
- [Recoverable Errors with `Result`](ch09-02-recoverable-errors-with-result.md)
|
||||
- [To `panic!` or Not To `panic!`](ch09-03-to-panic-or-not-to-panic.md)
|
||||
|
||||
- [Generic Types, Traits, and Lifetimes](ch10-00-generics.md)
|
||||
- [Generic Data Types](ch10-01-syntax.md)
|
||||
- [Traits: Defining Shared Behavior](ch10-02-traits.md)
|
||||
- [Validating References with Lifetimes](ch10-03-lifetime-syntax.md)
|
||||
|
||||
- [Testing](ch11-00-testing.md)
|
||||
- [Writing tests](ch11-01-writing-tests.md)
|
||||
- [Running tests](ch11-02-running-tests.md)
|
||||
- [Test Organization](ch11-03-test-organization.md)
|
||||
|
||||
- [An I/O Project: Building a Command Line Program](ch12-00-an-io-project.md)
|
||||
- [Accepting Command Line Arguments](ch12-01-accepting-command-line-arguments.md)
|
||||
- [Reading a File](ch12-02-reading-a-file.md)
|
||||
- [Refactoring to Improve Modularity and Error Handling](ch12-03-improving-error-handling-and-modularity.md)
|
||||
- [Developing the Library’s Functionality with Test Driven Development](ch12-04-testing-the-librarys-functionality.md)
|
||||
- [Working with Environment Variables](ch12-05-working-with-environment-variables.md)
|
||||
- [Writing Error Messages to Standard Error Instead of Standard Output](ch12-06-writing-to-stderr-instead-of-stdout.md)
|
||||
|
||||
## Thinking in Rust
|
||||
|
||||
- [Functional Language Features: Iterators and Closures](ch13-00-functional-features.md)
|
||||
- [Closures: Anonymous Functions that Can Capture Their Environment](ch13-01-closures.md)
|
||||
- [Processing a Series of Items with Iterators](ch13-02-iterators.md)
|
||||
- [Improving Our I/O Project](ch13-03-improving-our-io-project.md)
|
||||
- [Comparing Performance: Loops vs. Iterators](ch13-04-performance.md)
|
||||
|
||||
- [More about Cargo and Crates.io](ch14-00-more-about-cargo.md)
|
||||
- [Customizing Builds with Release Profiles](ch14-01-release-profiles.md)
|
||||
- [Publishing a Crate to Crates.io](ch14-02-publishing-to-crates-io.md)
|
||||
- [Cargo Workspaces](ch14-03-cargo-workspaces.md)
|
||||
- [Installing Binaries from Crates.io with `cargo install`](ch14-04-installing-binaries.md)
|
||||
- [Extending Cargo with Custom Commands](ch14-05-extending-cargo.md)
|
||||
|
||||
- [Smart Pointers](ch15-00-smart-pointers.md)
|
||||
- [`Box<T>` Points to Data on the Heap and Has a Known Size](ch15-01-box.md)
|
||||
- [The `Deref` Trait Allows Access to the Data Through a Reference](ch15-02-deref.md)
|
||||
- [The `Drop` Trait Runs Code on Cleanup](ch15-03-drop.md)
|
||||
- [`Rc<T>`, the Reference Counted Smart Pointer](ch15-04-rc.md)
|
||||
- [`RefCell<T>` and the Interior Mutability Pattern](ch15-05-interior-mutability.md)
|
||||
- [Creating Reference Cycles and Leaking Memory is Safe](ch15-06-reference-cycles.md)
|
||||
|
||||
- [Fearless Concurrency](ch16-00-concurrency.md)
|
||||
- [Threads](ch16-01-threads.md)
|
||||
- [Message Passing](ch16-02-message-passing.md)
|
||||
- [Shared State](ch16-03-shared-state.md)
|
||||
- [Extensible Concurrency: `Sync` and `Send`](ch16-04-extensible-concurrency-sync-and-send.md)
|
||||
|
||||
- [Is Rust an Object-Oriented Programming Language?](ch17-00-oop.md)
|
||||
- [What Does Object-Oriented Mean?](ch17-01-what-is-oo.md)
|
||||
- [Trait Objects for Using Values of Different Types](ch17-02-trait-objects.md)
|
||||
- [Object-Oriented Design Pattern Implementations](ch17-03-oo-design-patterns.md)
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
- [Patterns Match the Structure of Values](ch18-00-patterns.md)
|
||||
- [All the Places Patterns May be Used](ch18-01-all-the-places-for-patterns.md)
|
||||
- [Refutability: Whether a Pattern Might Fail to Match](ch18-02-refutability.md)
|
||||
- [All the Pattern Syntax](ch18-03-pattern-syntax.md)
|
||||
|
||||
- [Advanced Features](ch19-00-advanced-features.md)
|
||||
- [Unsafe Rust](ch19-01-unsafe-rust.md)
|
||||
- [Advanced Lifetimes](ch19-02-advanced-lifetimes.md)
|
||||
- [Advanced Traits](ch19-03-advanced-traits.md)
|
||||
- [Advanced Types](ch19-04-advanced-types.md)
|
||||
- [Advanced Functions & Closures](ch19-05-advanced-functions-and-closures.md)
|
||||
|
||||
- [Final Project: Building a Multithreaded Web Server](ch20-00-final-project-a-web-server.md)
|
||||
- [A Single Threaded Web Server](ch20-01-single-threaded.md)
|
||||
- [How Slow Requests Affect Throughput](ch20-02-slow-requests.md)
|
||||
- [Designing the Thread Pool Interface](ch20-03-designing-the-interface.md)
|
||||
- [Creating the Thread Pool and Storing Threads](ch20-04-storing-threads.md)
|
||||
- [Sending Requests to Threads Via Channels](ch20-05-sending-requests-via-channels.md)
|
||||
- [Graceful Shutdown and Cleanup](ch20-06-graceful-shutdown-and-cleanup.md)
|
||||
|
||||
- [Appendix](appendix-00.md)
|
||||
- [A - Keywords](appendix-01-keywords.md)
|
||||
- [B - Operators and Symbols](appendix-02-operators.md)
|
||||
- [C - Derivable Traits](appendix-03-derivable-traits.md)
|
||||
- [D - Macros](appendix-04-macros.md)
|
||||
- [E - Translations](appendix-05-translation.md)
|
||||
- [F - Newest Features](appendix-06-newest-features.md)
|
||||
@@ -1,47 +0,0 @@
|
||||
mod dummy_book;
|
||||
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
||||
use mdbook::MDBook;
|
||||
|
||||
#[test]
|
||||
fn mdbook_can_correctly_test_a_passing_book() {
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
|
||||
let result = md.test(vec![]);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Tests failed with {}",
|
||||
result.err().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_detects_book_with_failing_tests() {
|
||||
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
|
||||
assert!(md.test(vec![]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_test_chapter() {
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
|
||||
let result = md.test_chapter(vec![], Some("Introduction"));
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"test_chapter failed with {}",
|
||||
result.err().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_test_chapter_not_found() {
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
|
||||
assert!(md.test_chapter(vec![], Some("Bogus Chapter Name")).is_err());
|
||||
}
|
||||
43
tests/testsuite/README.md
Normal file
43
tests/testsuite/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Testsuite
|
||||
|
||||
## Introduction
|
||||
|
||||
This is the main testsuite for exercising all functionality of mdBook.
|
||||
|
||||
Tests should be organized into modules based around major features. Tests should use `BookTest` to drive the test. `BookTest` will set up a temp directory, and provides a variety of methods to help create a build books.
|
||||
|
||||
## Basic structure of a test
|
||||
|
||||
Using `BookTest`, you typically use it to copy a directory into a temp directory, and then run mdbook commands in that temp directory. You can run the `mdbook` executable, or use the mdbook API to perform whatever tasks you need. Running the executable has the benefit of being able to validate the console output.
|
||||
|
||||
See `build::basic_build` for a simple test example. I recommend reviewing the methods on `BookTest` to learn more, and reviewing some of the existing tests to get a feel for how they are structured.
|
||||
|
||||
For example, let's say you are creating a new theme test. In the `testsuite/theme` directory, create a new directory with the book source that you want to exercise. At a minimum, this needs a `src/SUMMARY.md`, but often you'll also want `book.toml`. Then, in `testsuite/theme.rs`, add a test with `BookTest::from_dir("theme/mytest")`, and then use the methods to perform whatever actions you want.
|
||||
|
||||
`BookTest` is designed to be able to chain a series of actions. For example, you can do something like:
|
||||
|
||||
```rust
|
||||
BookTest::from_dir("theme/mytest")
|
||||
.build()
|
||||
.check_main_file("book/index.html", str![["file contents"]])
|
||||
.change_file("src/index.md", "new contents")
|
||||
.build()
|
||||
.check_main_file("book/index.html", str![["new contents"]]);
|
||||
```
|
||||
|
||||
## Snapbox
|
||||
|
||||
The testsuite uses [`snapbox`] to drive most of the tests. This library provides the ability to compare strings using a variety of methods. These strings are written in the source code using either the [`str!`] or [`file!`] macros.
|
||||
|
||||
The magic is that you can set the `SNAPSHOTS=overwrite` environment variable, and snapbox will automatically update the strings contents of `str!`, or the file contents of `file!`. This makes it easier to update tests. Snapbox provides nice diffing output, and quite a few other features.
|
||||
|
||||
Expected contents can have wildcards like `...` (matches any lines) or `[..]` (matches any characters on a line). See [snapbox filters] for more info and other filters.
|
||||
|
||||
Typically when writing a test, I'll just start with an empty `str!` or `file!`, and let snapbox fill it in. Then I review the contents to make sure they are what I expect.
|
||||
|
||||
Note that there is some normalization applied to the strings. See `book_test::assert` for how some of these normalizations happen.
|
||||
|
||||
[`snapbox`]: https://docs.rs/snapbox/latest/snapbox/
|
||||
[`str!`]: https://docs.rs/snapbox/latest/snapbox/macro.str.html
|
||||
[`file!`]: https://docs.rs/snapbox/latest/snapbox/macro.file.html
|
||||
[snapbox filters]: https://docs.rs/snapbox/latest/snapbox/assert/struct.Assert.html#method.eq
|
||||
442
tests/testsuite/book_test.rs
Normal file
442
tests/testsuite/book_test.rs
Normal file
@@ -0,0 +1,442 @@
|
||||
//! Utility for building and running tests against mdbook.
|
||||
|
||||
use mdbook::book::BookBuilder;
|
||||
use mdbook::MDBook;
|
||||
use snapbox::IntoData;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
/// Test number used for generating unique temp directory names.
|
||||
static NEXT_TEST_ID: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
enum StatusCode {
|
||||
Success,
|
||||
Failure,
|
||||
Code(i32),
|
||||
}
|
||||
|
||||
/// Main helper for driving mdbook tests.
|
||||
pub struct BookTest {
|
||||
/// The temp directory where the test should perform its work.
|
||||
pub dir: PathBuf,
|
||||
assert: snapbox::Assert,
|
||||
/// This indicates whether or not the book has been built.
|
||||
built: bool,
|
||||
}
|
||||
|
||||
impl BookTest {
|
||||
/// Creates a new test, copying the contents from the given directory into
|
||||
/// a temp directory.
|
||||
pub fn from_dir(dir: &str) -> BookTest {
|
||||
// Copy this test book to a temp directory.
|
||||
let dir = Path::new("tests/testsuite").join(dir);
|
||||
assert!(dir.exists(), "{dir:?} should exist");
|
||||
let tmp = Self::new_tmp();
|
||||
mdbook::utils::fs::copy_files_except_ext(
|
||||
&dir,
|
||||
&tmp,
|
||||
true,
|
||||
Some(&PathBuf::from("book")),
|
||||
&[],
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("failed to copy test book {dir:?} to {tmp:?}: {e:?}"));
|
||||
Self::new(tmp)
|
||||
}
|
||||
|
||||
/// Creates a new test with an empty temp directory.
|
||||
pub fn empty() -> BookTest {
|
||||
Self::new(Self::new_tmp())
|
||||
}
|
||||
|
||||
/// Creates a new test with the given function to initialize a new book.
|
||||
///
|
||||
/// The book itself is not built.
|
||||
pub fn init(f: impl Fn(&mut BookBuilder)) -> BookTest {
|
||||
let tmp = Self::new_tmp();
|
||||
let mut bb = MDBook::init(&tmp);
|
||||
f(&mut bb);
|
||||
bb.build()
|
||||
.unwrap_or_else(|e| panic!("failed to initialize book at {tmp:?}: {e:?}"));
|
||||
Self::new(tmp)
|
||||
}
|
||||
|
||||
fn new_tmp() -> PathBuf {
|
||||
let id = NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let tmp = Path::new(env!("CARGO_TARGET_TMPDIR"))
|
||||
.join("ts")
|
||||
.join(format!("t{id}"));
|
||||
if tmp.exists() {
|
||||
std::fs::remove_dir_all(&tmp)
|
||||
.unwrap_or_else(|e| panic!("failed to remove {tmp:?}: {e:?}"));
|
||||
}
|
||||
std::fs::create_dir_all(&tmp).unwrap_or_else(|e| panic!("failed to create {tmp:?}: {e:?}"));
|
||||
tmp
|
||||
}
|
||||
|
||||
fn new(dir: PathBuf) -> BookTest {
|
||||
let assert = assert(&dir);
|
||||
BookTest {
|
||||
dir,
|
||||
assert,
|
||||
built: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks the contents of an HTML file that it has the given contents
|
||||
/// between the `<main>` tag.
|
||||
///
|
||||
/// Normally the contents outside of the `<main>` tag aren't interesting,
|
||||
/// and they add a significant amount of noise.
|
||||
pub fn check_main_file(&mut self, path: &str, expected: impl IntoData) -> &mut Self {
|
||||
if !self.built {
|
||||
self.build();
|
||||
}
|
||||
let full_path = self.dir.join(path);
|
||||
let actual = read_to_string(&full_path);
|
||||
let start = actual
|
||||
.find("<main>")
|
||||
.unwrap_or_else(|| panic!("didn't find <main> in:\n{actual}"));
|
||||
let end = actual.find("</main>").unwrap();
|
||||
let contents = actual[start + 6..end - 7].trim();
|
||||
self.assert.eq(contents, expected);
|
||||
self
|
||||
}
|
||||
|
||||
/// Checks the summary contents of `toc.js` against the expected value.
|
||||
pub fn check_toc_js(&mut self, expected: impl IntoData) -> &mut Self {
|
||||
if !self.built {
|
||||
self.build();
|
||||
}
|
||||
let inner = self.toc_js_html();
|
||||
// Would be nice if this were prettified, but a primitive wrapping will do for now.
|
||||
let inner = inner.replace("><", ">\n<");
|
||||
self.assert.eq(inner, expected);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the summary contents from `toc.js`.
|
||||
pub fn toc_js_html(&self) -> String {
|
||||
let full_path = self.dir.join("book/toc.js");
|
||||
let actual = read_to_string(&full_path);
|
||||
let inner = actual
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.trim().strip_prefix("this.innerHTML = '")?;
|
||||
let line = line.strip_suffix("';")?;
|
||||
Some(line)
|
||||
})
|
||||
.next()
|
||||
.expect("should have innerHTML");
|
||||
inner.to_string()
|
||||
}
|
||||
|
||||
/// Checks that the contents of the given file matches the expected value.
|
||||
pub fn check_file(&mut self, path: &str, expected: impl IntoData) -> &mut Self {
|
||||
if !self.built {
|
||||
self.build();
|
||||
}
|
||||
let path = self.dir.join(path);
|
||||
let actual = read_to_string(&path);
|
||||
self.assert.eq(actual, expected);
|
||||
self
|
||||
}
|
||||
|
||||
/// Checks that the given file contains the given string somewhere.
|
||||
pub fn check_file_contains(&mut self, path: &str, expected: &str) -> &mut Self {
|
||||
if !self.built {
|
||||
self.build();
|
||||
}
|
||||
let path = self.dir.join(path);
|
||||
let actual = read_to_string(&path);
|
||||
assert!(
|
||||
actual.contains(expected),
|
||||
"Did not find {expected:?} in {path:?}\n\n{actual}",
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Checks that the given file does not contain the given string anywhere.
|
||||
///
|
||||
/// Beware that using this is fragile, as it may be unable to catch
|
||||
/// regressions (it can't tell the difference between success, or the
|
||||
/// string being looked for changed).
|
||||
pub fn check_file_doesnt_contain(&mut self, path: &str, string: &str) -> &mut Self {
|
||||
if !self.built {
|
||||
self.build();
|
||||
}
|
||||
let path = self.dir.join(path);
|
||||
let actual = read_to_string(&path);
|
||||
assert!(
|
||||
!actual.contains(string),
|
||||
"Unexpectedly found {string:?} in {path:?}\n\n{actual}",
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Checks that the list of files at the given path matches the given value.
|
||||
pub fn check_file_list(&mut self, path: &str, expected: impl IntoData) -> &mut Self {
|
||||
let mut all_paths: Vec<_> = walkdir::WalkDir::new(&self.dir.join(path))
|
||||
.into_iter()
|
||||
// Skip the outer directory.
|
||||
.skip(1)
|
||||
.map(|e| {
|
||||
e.unwrap()
|
||||
.into_path()
|
||||
.strip_prefix(&self.dir)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace('\\', "/")
|
||||
})
|
||||
.collect();
|
||||
all_paths.sort();
|
||||
let actual = all_paths.join("\n");
|
||||
self.assert.eq(actual, expected);
|
||||
self
|
||||
}
|
||||
|
||||
/// Loads an [`MDBook`] from the temp directory.
|
||||
pub fn load_book(&self) -> MDBook {
|
||||
MDBook::load(&self.dir).unwrap_or_else(|e| panic!("book failed to load: {e:?}"))
|
||||
}
|
||||
|
||||
/// Builds the book in the temp directory.
|
||||
pub fn build(&mut self) -> &mut Self {
|
||||
let book = self.load_book();
|
||||
book.build()
|
||||
.unwrap_or_else(|e| panic!("book failed to build: {e:?}"));
|
||||
self.built = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs the `mdbook` binary in the temp directory.
|
||||
///
|
||||
/// This runs `mdbook` with the given args. The args are split on spaces
|
||||
/// (if you need args with spaces, use the `args` method). The given
|
||||
/// callback receives a [`BookCommand`] for you to customize how the
|
||||
/// executable is run.
|
||||
pub fn run(&mut self, args: &str, f: impl Fn(&mut BookCommand)) -> &mut Self {
|
||||
let mut cmd = BookCommand {
|
||||
assert: self.assert.clone(),
|
||||
dir: self.dir.clone(),
|
||||
args: split_args(args),
|
||||
env: BTreeMap::new(),
|
||||
expect_status: StatusCode::Success,
|
||||
expect_stderr_data: None,
|
||||
expect_stdout_data: None,
|
||||
};
|
||||
f(&mut cmd);
|
||||
cmd.run();
|
||||
self
|
||||
}
|
||||
|
||||
/// Change a file's contents in the given path.
|
||||
pub fn change_file(&mut self, path: impl AsRef<Path>, body: &str) -> &mut Self {
|
||||
let path = self.dir.join(path);
|
||||
std::fs::write(&path, body).unwrap_or_else(|e| panic!("failed to write {path:?}: {e:?}"));
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds a Rust program with the given src.
|
||||
///
|
||||
/// The given path should be the path where to output the executable in
|
||||
/// the temp directory.
|
||||
pub fn rust_program(&mut self, path: &str, src: &str) -> &mut Self {
|
||||
let rs = self.dir.join(path).with_extension("rs");
|
||||
let parent = rs.parent().unwrap();
|
||||
if !parent.exists() {
|
||||
std::fs::create_dir_all(&parent).unwrap();
|
||||
}
|
||||
std::fs::write(&rs, src).unwrap_or_else(|e| panic!("failed to write {rs:?}: {e:?}"));
|
||||
let status = std::process::Command::new("rustc")
|
||||
.arg(&rs)
|
||||
.current_dir(&parent)
|
||||
.status()
|
||||
.expect("rustc should run");
|
||||
assert!(status.success());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for preparing to run the `mdbook` executable.
|
||||
///
|
||||
/// By default, it expects the process to succeed.
|
||||
pub struct BookCommand {
|
||||
pub dir: PathBuf,
|
||||
assert: snapbox::Assert,
|
||||
args: Vec<String>,
|
||||
env: BTreeMap<String, Option<String>>,
|
||||
expect_status: StatusCode,
|
||||
expect_stderr_data: Option<snapbox::Data>,
|
||||
expect_stdout_data: Option<snapbox::Data>,
|
||||
}
|
||||
|
||||
impl BookCommand {
|
||||
/// Indicates that the process should fail.
|
||||
pub fn expect_failure(&mut self) -> &mut Self {
|
||||
self.expect_status = StatusCode::Failure;
|
||||
self
|
||||
}
|
||||
|
||||
/// Indicates the process should fail with the given exit code.
|
||||
pub fn expect_code(&mut self, code: i32) -> &mut Self {
|
||||
self.expect_status = StatusCode::Code(code);
|
||||
self
|
||||
}
|
||||
|
||||
/// Verifies that stderr matches the given value.
|
||||
pub fn expect_stderr(&mut self, expected: impl snapbox::IntoData) -> &mut Self {
|
||||
self.expect_stderr_data = Some(expected.into_data());
|
||||
self
|
||||
}
|
||||
|
||||
/// Verifies that stdout matches the given value.
|
||||
pub fn expect_stdout(&mut self, expected: impl snapbox::IntoData) -> &mut Self {
|
||||
self.expect_stdout_data = Some(expected.into_data());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds arguments to the command to run.
|
||||
pub fn args(&mut self, args: &[&str]) -> &mut Self {
|
||||
self.args.extend(args.into_iter().map(|t| t.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies an environment variable to set on the executable.
|
||||
pub fn env<T: Into<String>>(&mut self, key: &str, value: T) -> &mut Self {
|
||||
self.env.insert(key.to_string(), Some(value.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs the command, and verifies the output.
|
||||
fn run(&mut self) {
|
||||
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mdbook"));
|
||||
cmd.current_dir(&self.dir)
|
||||
.args(&self.args)
|
||||
.env_remove("RUST_LOG")
|
||||
// Don't read the system git config which is out of our control.
|
||||
.env("GIT_CONFIG_NOSYSTEM", "1")
|
||||
.env("GIT_CONFIG_GLOBAL", &self.dir)
|
||||
.env("GIT_CONFIG_SYSTEM", &self.dir)
|
||||
.env_remove("GIT_AUTHOR_EMAIL")
|
||||
.env_remove("GIT_AUTHOR_NAME")
|
||||
.env_remove("GIT_COMMITTER_EMAIL")
|
||||
.env_remove("GIT_COMMITTER_NAME");
|
||||
|
||||
for (k, v) in &self.env {
|
||||
match v {
|
||||
Some(v) => cmd.env(k, v),
|
||||
None => cmd.env_remove(k),
|
||||
};
|
||||
}
|
||||
|
||||
let output = cmd.output().expect("mdbook should be runnable");
|
||||
let stdout = std::str::from_utf8(&output.stdout).expect("stdout is not utf8");
|
||||
let stderr = std::str::from_utf8(&output.stderr).expect("stderr is not utf8");
|
||||
let render_output = || format!("\n--- stdout\n{stdout}\n--- stderr\n{stderr}");
|
||||
match (self.expect_status, output.status.success()) {
|
||||
(StatusCode::Success, false) => {
|
||||
panic!("mdbook failed, but expected success{}", render_output())
|
||||
}
|
||||
(StatusCode::Failure, true) => {
|
||||
panic!("mdbook succeeded, but expected failure{}", render_output())
|
||||
}
|
||||
(StatusCode::Code(expected), _) => match output.status.code() {
|
||||
Some(actual) => assert_eq!(
|
||||
actual, expected,
|
||||
"process exit code did not match as expected"
|
||||
),
|
||||
None => panic!("process exited via signal {:?}", output.status),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
self.expect_status = StatusCode::Success; // Reset to default.
|
||||
if let Some(expect_stderr_data) = &self.expect_stderr_data {
|
||||
if let Err(e) = self.assert.try_eq(
|
||||
Some(&"stderr"),
|
||||
stderr.into_data(),
|
||||
expect_stderr_data.clone(),
|
||||
) {
|
||||
panic!("{e}");
|
||||
}
|
||||
}
|
||||
if let Some(expect_stdout_data) = &self.expect_stdout_data {
|
||||
if let Err(e) = self.assert.try_eq(
|
||||
Some(&"stdout"),
|
||||
stdout.into_data(),
|
||||
expect_stdout_data.clone(),
|
||||
) {
|
||||
panic!("{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split_args(s: &str) -> Vec<String> {
|
||||
s.split_whitespace()
|
||||
.map(|arg| {
|
||||
if arg.contains(&['"', '\''][..]) {
|
||||
panic!("shell-style argument parsing is not supported");
|
||||
}
|
||||
String::from(arg)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
static LITERAL_REDACTIONS: &[(&str, &str)] = &[
|
||||
// Unix message for an entity was not found
|
||||
("[NOT_FOUND]", "No such file or directory (os error 2)"),
|
||||
// Windows message for an entity was not found
|
||||
(
|
||||
"[NOT_FOUND]",
|
||||
"The system cannot find the file specified. (os error 2)",
|
||||
),
|
||||
(
|
||||
"[NOT_FOUND]",
|
||||
"The system cannot find the path specified. (os error 3)",
|
||||
),
|
||||
("[NOT_FOUND]", "program not found"),
|
||||
// Unix message for exit status
|
||||
("[EXIT_STATUS]", "exit status"),
|
||||
// Windows message for exit status
|
||||
("[EXIT_STATUS]", "exit code"),
|
||||
("[TAB]", "\t"),
|
||||
("[EXE]", std::env::consts::EXE_SUFFIX),
|
||||
];
|
||||
|
||||
/// This makes it easier to write regex replacements that are guaranteed to only
|
||||
/// get compiled once
|
||||
macro_rules! regex {
|
||||
($re:literal $(,)?) => {{
|
||||
static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
|
||||
RE.get_or_init(|| regex::Regex::new($re).unwrap())
|
||||
}};
|
||||
}
|
||||
|
||||
fn assert(root: &Path) -> snapbox::Assert {
|
||||
let mut subs = snapbox::Redactions::new();
|
||||
subs.insert("[ROOT]", root.to_path_buf()).unwrap();
|
||||
subs.insert(
|
||||
"[TIMESTAMP]",
|
||||
regex!(r"(?m)(?<redacted>20\d\d-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"),
|
||||
)
|
||||
.unwrap();
|
||||
subs.insert("[VERSION]", mdbook::MDBOOK_VERSION).unwrap();
|
||||
|
||||
subs.extend(LITERAL_REDACTIONS.into_iter().cloned())
|
||||
.unwrap();
|
||||
|
||||
snapbox::Assert::new()
|
||||
.action_env(snapbox::assert::DEFAULT_ACTION_ENV)
|
||||
.redact_with(subs)
|
||||
}
|
||||
|
||||
/// Helper to read a string from the filesystem.
|
||||
#[track_caller]
|
||||
pub fn read_to_string<P: AsRef<Path>>(path: P) -> String {
|
||||
let path = path.as_ref();
|
||||
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("could not read file {path:?}: {e:?}"))
|
||||
}
|
||||
67
tests/testsuite/build.rs
Normal file
67
tests/testsuite/build.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
//! General build tests.
|
||||
//!
|
||||
//! More specific tests should usually go into a module based on the feature.
|
||||
//! This module should just have general build tests, or misc small things.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
// Simple smoke test that building works.
|
||||
#[test]
|
||||
fn basic_build() {
|
||||
BookTest::from_dir("build/basic_build").run("build", |cmd| {
|
||||
cmd.expect_stderr(str![[r#"
|
||||
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
|
||||
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
|
||||
|
||||
"#]]);
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure building fails if `create-missing` is false and one of the files does
|
||||
// not exist.
|
||||
#[test]
|
||||
fn failure_on_missing_file() {
|
||||
BookTest::from_dir("build/missing_file").run("build", |cmd| {
|
||||
cmd.expect_failure().expect_stderr(str![[r#"
|
||||
[TIMESTAMP] [ERROR] (mdbook::utils): Error: Chapter file not found, ./chapter_1.md
|
||||
[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: [NOT_FOUND]
|
||||
|
||||
"#]]);
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure a missing file is created if `create-missing` is true.
|
||||
#[test]
|
||||
fn create_missing() {
|
||||
let test = BookTest::from_dir("build/create_missing");
|
||||
assert!(test.dir.join("src/SUMMARY.md").exists());
|
||||
assert!(!test.dir.join("src/chapter_1.md").exists());
|
||||
test.load_book();
|
||||
assert!(test.dir.join("src/chapter_1.md").exists());
|
||||
}
|
||||
|
||||
// Checks that it fails if the summary has a reserved filename.
|
||||
#[test]
|
||||
fn no_reserved_filename() {
|
||||
BookTest::from_dir("build/no_reserved_filename").run("build", |cmd| {
|
||||
cmd.expect_failure().expect_stderr(str![[r#"
|
||||
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
|
||||
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
|
||||
[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed
|
||||
[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: print.md is reserved for internal use
|
||||
|
||||
"#]]);
|
||||
});
|
||||
}
|
||||
|
||||
// Build without book.toml should be OK.
|
||||
#[test]
|
||||
fn book_toml_isnt_required() {
|
||||
let mut test = BookTest::init(|_| {});
|
||||
std::fs::remove_file(test.dir.join("book.toml")).unwrap();
|
||||
test.build();
|
||||
test.check_main_file(
|
||||
"book/chapter_1.html",
|
||||
str![[r##"<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>"##]],
|
||||
);
|
||||
}
|
||||
2
tests/testsuite/build/basic_build/book.toml
Normal file
2
tests/testsuite/build/basic_build/book.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[book]
|
||||
title = "basic_build"
|
||||
3
tests/testsuite/build/basic_build/src/SUMMARY.md
Normal file
3
tests/testsuite/build/basic_build/src/SUMMARY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Summary
|
||||
|
||||
- [Chapter 1](./chapter_1.md)
|
||||
2
tests/testsuite/build/create_missing/book.toml
Normal file
2
tests/testsuite/build/create_missing/book.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[book]
|
||||
title = "create_missing"
|
||||
3
tests/testsuite/build/create_missing/src/SUMMARY.md
Normal file
3
tests/testsuite/build/create_missing/src/SUMMARY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Summary
|
||||
|
||||
- [Chapter 1](./chapter_1.md)
|
||||
5
tests/testsuite/build/missing_file/book.toml
Normal file
5
tests/testsuite/build/missing_file/book.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[book]
|
||||
title = "missing_file"
|
||||
|
||||
[build]
|
||||
create-missing = false
|
||||
3
tests/testsuite/build/missing_file/src/SUMMARY.md
Normal file
3
tests/testsuite/build/missing_file/src/SUMMARY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Summary
|
||||
|
||||
- [Chapter 1](./chapter_1.md)
|
||||
2
tests/testsuite/build/no_reserved_filename/book.toml
Normal file
2
tests/testsuite/build/no_reserved_filename/book.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[book]
|
||||
title = "no_reserved_filename"
|
||||
@@ -0,0 +1,3 @@
|
||||
# Summary
|
||||
|
||||
- [Print](print.md)
|
||||
1
tests/testsuite/build/no_reserved_filename/src/print.md
Normal file
1
tests/testsuite/build/no_reserved_filename/src/print.md
Normal file
@@ -0,0 +1 @@
|
||||
# Print
|
||||
36
tests/testsuite/cli.rs
Normal file
36
tests/testsuite/cli.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
//! Basic tests for mdbook's CLI.
|
||||
|
||||
use crate::prelude::*;
|
||||
use snapbox::file;
|
||||
|
||||
// Test with no args.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
not(all(feature = "watch", feature = "serve")),
|
||||
ignore = "needs all features"
|
||||
)]
|
||||
fn no_args() {
|
||||
BookTest::empty().run("", |cmd| {
|
||||
cmd.expect_code(2)
|
||||
.expect_stdout(str![[""]])
|
||||
.expect_stderr(file!["cli/no_args.term.svg"]);
|
||||
});
|
||||
}
|
||||
|
||||
// Help command.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
not(all(feature = "watch", feature = "serve")),
|
||||
ignore = "needs all features"
|
||||
)]
|
||||
fn help() {
|
||||
BookTest::empty()
|
||||
.run("help", |cmd| {
|
||||
cmd.expect_stdout(file!["cli/help.term.svg"])
|
||||
.expect_stderr(str![[""]]);
|
||||
})
|
||||
.run("--help", |cmd| {
|
||||
cmd.expect_stdout(file!["cli/help.term.svg"])
|
||||
.expect_stderr(str![[""]]);
|
||||
});
|
||||
}
|
||||
63
tests/testsuite/cli/help.term.svg
Normal file
63
tests/testsuite/cli/help.term.svg
Normal file
@@ -0,0 +1,63 @@
|
||||
<svg width="740px" height="398px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
line-height: 18px;
|
||||
}
|
||||
tspan {
|
||||
font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
|
||||
white-space: pre;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
|
||||
|
||||
<text xml:space="preserve" class="container fg">
|
||||
<tspan x="10px" y="28px"><tspan>Creates a book from markdown files</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="46px">
|
||||
</tspan>
|
||||
<tspan x="10px" y="64px"><tspan>Usage: mdbook[EXE] [COMMAND]</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="82px">
|
||||
</tspan>
|
||||
<tspan x="10px" y="100px"><tspan>Commands:</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="118px"><tspan> init Creates the boilerplate structure and files for a new book</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="136px"><tspan> build Builds a book from its markdown files</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="154px"><tspan> test Tests that a book's Rust code samples compile</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="172px"><tspan> clean Deletes a built book</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="190px"><tspan> completions Generate shell completions for your shell to stdout</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="208px"><tspan> watch Watches a book's files and rebuilds it on changes</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="226px"><tspan> serve Serves a book at http://localhost:3000, and rebuilds it on changes</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="244px"><tspan> help Print this message or the help of the given subcommand(s)</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="262px">
|
||||
</tspan>
|
||||
<tspan x="10px" y="280px"><tspan>Options:</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="298px"><tspan> -h, --help Print help</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="316px"><tspan> -V, --version Print version</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="334px">
|
||||
</tspan>
|
||||
<tspan x="10px" y="352px"><tspan>For more information about a specific command, try `mdbook <command> --help`</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="370px"><tspan>The source code for mdBook is available at: https://github.com/rust-lang/mdBook</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="388px">
|
||||
</tspan>
|
||||
</text>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
63
tests/testsuite/cli/no_args.term.svg
Normal file
63
tests/testsuite/cli/no_args.term.svg
Normal file
@@ -0,0 +1,63 @@
|
||||
<svg width="740px" height="398px" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.fg { fill: #AAAAAA }
|
||||
.bg { background: #000000 }
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
line-height: 18px;
|
||||
}
|
||||
tspan {
|
||||
font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
|
||||
white-space: pre;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
|
||||
|
||||
<text xml:space="preserve" class="container fg">
|
||||
<tspan x="10px" y="28px"><tspan>Creates a book from markdown files</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="46px">
|
||||
</tspan>
|
||||
<tspan x="10px" y="64px"><tspan>Usage: mdbook[EXE] [COMMAND]</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="82px">
|
||||
</tspan>
|
||||
<tspan x="10px" y="100px"><tspan>Commands:</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="118px"><tspan> init Creates the boilerplate structure and files for a new book</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="136px"><tspan> build Builds a book from its markdown files</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="154px"><tspan> test Tests that a book's Rust code samples compile</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="172px"><tspan> clean Deletes a built book</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="190px"><tspan> completions Generate shell completions for your shell to stdout</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="208px"><tspan> watch Watches a book's files and rebuilds it on changes</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="226px"><tspan> serve Serves a book at http://localhost:3000, and rebuilds it on changes</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="244px"><tspan> help Print this message or the help of the given subcommand(s)</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="262px">
|
||||
</tspan>
|
||||
<tspan x="10px" y="280px"><tspan>Options:</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="298px"><tspan> -h, --help Print help</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="316px"><tspan> -V, --version Print version</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="334px">
|
||||
</tspan>
|
||||
<tspan x="10px" y="352px"><tspan>For more information about a specific command, try `mdbook <command> --help`</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="370px"><tspan>The source code for mdBook is available at: https://github.com/rust-lang/mdBook</tspan>
|
||||
</tspan>
|
||||
<tspan x="10px" y="388px">
|
||||
</tspan>
|
||||
</text>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
112
tests/testsuite/includes.rs
Normal file
112
tests/testsuite/includes.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! Tests for include preprocessor.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
// Basic test for #include.
|
||||
#[test]
|
||||
fn include() {
|
||||
BookTest::from_dir("includes/all_includes")
|
||||
.check_main_file(
|
||||
"book/includes.html",
|
||||
str![[r##"
|
||||
<h1 id="basic-includes"><a class="header" href="#basic-includes">Basic Includes</a></h1>
|
||||
<h2 id="sample"><a class="header" href="#sample">Sample</a></h2>
|
||||
<p>This is a sample include.</p>
|
||||
"##]],
|
||||
)
|
||||
.check_main_file(
|
||||
"book/relative/includes.html",
|
||||
str![[r##"
|
||||
<h1 id="relative-includes"><a class="header" href="#relative-includes">Relative Includes</a></h1>
|
||||
<h2 id="sample"><a class="header" href="#sample">Sample</a></h2>
|
||||
<p>This is a sample include.</p>
|
||||
"##]],
|
||||
);
|
||||
}
|
||||
|
||||
// Checks for anchored includes.
|
||||
#[test]
|
||||
fn anchored_include() {
|
||||
BookTest::from_dir("includes/all_includes").check_main_file(
|
||||
"book/anchors.html",
|
||||
str![[r##"
|
||||
<h1 id="include-anchors"><a class="header" href="#include-anchors">Include Anchors</a></h1>
|
||||
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
|
||||
</span><span class="boring">fn main() {
|
||||
</span>let x = 1;
|
||||
<span class="boring">}</span></code></pre></pre>
|
||||
"##]],
|
||||
);
|
||||
}
|
||||
|
||||
// Checks behavior of recursive include.
|
||||
#[test]
|
||||
fn recursive_include() {
|
||||
BookTest::from_dir("includes/all_includes")
|
||||
.run("build", |cmd| {
|
||||
cmd.expect_stderr(str![[r#"
|
||||
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
|
||||
[TIMESTAMP] [ERROR] (mdbook::preprocess::links): Stack depth exceeded in recursive.md. Check for cyclic includes
|
||||
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
|
||||
|
||||
"#]]);
|
||||
})
|
||||
.check_main_file(
|
||||
"book/recursive.html",
|
||||
str![[r#"
|
||||
<p>Around the world, around the world
|
||||
Around the world, around the world
|
||||
Around the world, around the world
|
||||
Around the world, around the world
|
||||
Around the world, around the world
|
||||
Around the world, around the world
|
||||
Around the world, around the world
|
||||
Around the world, around the world
|
||||
Around the world, around the world
|
||||
Around the world, around the world
|
||||
Around the world, around the world</p>
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
// Checks the behavior of `{{#playground}}` include.
|
||||
#[test]
|
||||
fn playground_include() {
|
||||
BookTest::from_dir("includes/all_includes")
|
||||
.check_main_file("book/playground.html",
|
||||
str![[r##"
|
||||
<h1 id="playground-includes"><a class="header" href="#playground-includes">Playground Includes</a></h1>
|
||||
<pre><pre class="playground"><code class="language-rust">fn main() {
|
||||
println!("Hello World!");
|
||||
<span class="boring">
|
||||
</span><span class="boring"> // You can even hide lines! :D
|
||||
</span><span class="boring"> println!("I am hidden! Expand the code snippet to see me");
|
||||
</span>}</code></pre></pre>
|
||||
"##]]);
|
||||
}
|
||||
|
||||
// Checks the behavior of `{{#rustdoc_include}}`.
|
||||
#[test]
|
||||
fn rustdoc_include() {
|
||||
BookTest::from_dir("includes/all_includes")
|
||||
.check_main_file("book/rustdoc.html",
|
||||
str![[r##"
|
||||
<h1 id="rustdoc-includes"><a class="header" href="#rustdoc-includes">Rustdoc Includes</a></h1>
|
||||
<h2 id="rustdoc-include-adds-the-rest-of-the-file-as-hidden"><a class="header" href="#rustdoc-include-adds-the-rest-of-the-file-as-hidden">Rustdoc include adds the rest of the file as hidden</a></h2>
|
||||
<pre><pre class="playground"><code class="language-rust"><span class="boring">fn some_function() {
|
||||
</span><span class="boring"> println!("some function");
|
||||
</span><span class="boring">}
|
||||
</span><span class="boring">
|
||||
</span>fn main() {
|
||||
some_function();
|
||||
}</code></pre></pre>
|
||||
<h2 id="rustdoc-include-works-with-anchors-too"><a class="header" href="#rustdoc-include-works-with-anchors-too">Rustdoc include works with anchors too</a></h2>
|
||||
<pre><pre class="playground"><code class="language-rust"><span class="boring">fn some_other_function() {
|
||||
</span><span class="boring"> println!("unused anchor");
|
||||
</span><span class="boring">}
|
||||
</span><span class="boring">
|
||||
</span>fn main() {
|
||||
some_other_function();
|
||||
}</code></pre></pre>
|
||||
"##]]);
|
||||
}
|
||||
6
tests/testsuite/includes/all_includes/book.toml
Normal file
6
tests/testsuite/includes/all_includes/book.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[book]
|
||||
authors = ["Eric Huss"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "all_includes"
|
||||
8
tests/testsuite/includes/all_includes/src/SUMMARY.md
Normal file
8
tests/testsuite/includes/all_includes/src/SUMMARY.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Summary
|
||||
|
||||
- [Basic Includes](./includes.md)
|
||||
- [Relative Includes](./relative/includes.md)
|
||||
- [Recursive Includes](./recursive.md)
|
||||
- [Include Anchors](./anchors.md)
|
||||
- [Rustdoc Includes](./rustdoc.md)
|
||||
- [Playground Includes](./playground.md)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user