mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 16:12:33 -05:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07b25cdb64 | ||
|
|
105d836fbc | ||
|
|
74fcaf5273 | ||
|
|
74200f7395 | ||
|
|
1a5286b25c | ||
|
|
c493d3b5e3 | ||
|
|
a68091a84c | ||
|
|
b0cf568ba4 | ||
|
|
bf544be282 | ||
|
|
4f0dba8fdb | ||
|
|
5390e44dec | ||
|
|
e7e3317ff0 | ||
|
|
d68a596455 | ||
|
|
ace2abff34 | ||
|
|
0c6439faad | ||
|
|
e7418f21f9 | ||
|
|
19146c403e | ||
|
|
66ded2302f | ||
|
|
98abb22be1 | ||
|
|
ab304e7d38 | ||
|
|
fbc21592af | ||
|
|
e7b69114ed | ||
|
|
8a9ecd212d | ||
|
|
ec157cd1cd | ||
|
|
d3bcb359fa | ||
|
|
2a4e5583ab | ||
|
|
3978612611 | ||
|
|
4941acdb87 | ||
|
|
7e3d2f96ab | ||
|
|
ddba36b24c | ||
|
|
35cf96a064 | ||
|
|
5777a0edc4 | ||
|
|
53c3a92285 | ||
|
|
82db7f5b93 | ||
|
|
879449447f | ||
|
|
132ca0dca3 | ||
|
|
56c2b9ba3a | ||
|
|
542b6feed1 | ||
|
|
2af44a396f | ||
|
|
40d91fff29 | ||
|
|
59eab7cfc2 | ||
|
|
1b524ff356 | ||
|
|
9b873e9d97 | ||
|
|
b6d6cb2711 | ||
|
|
c8095160d0 | ||
|
|
ae6db3a87e | ||
|
|
18f57f5bd9 | ||
|
|
09a37284b0 | ||
|
|
dff5ac64e5 | ||
|
|
0ee565a5ff | ||
|
|
9e4854f349 | ||
|
|
74d48f5ad2 | ||
|
|
0b51a74c16 | ||
|
|
ce63cc31f4 | ||
|
|
d6720fc671 | ||
|
|
64cca1399b | ||
|
|
629c2ad2fd | ||
|
|
d325e821cd | ||
|
|
ac3a7faa54 | ||
|
|
35ed24cd18 | ||
|
|
81d42f1c6e | ||
|
|
618a2fa78b | ||
|
|
0bf6751eed | ||
|
|
f92eac4acd | ||
|
|
69ef52fd13 | ||
|
|
cc8ce35b4d | ||
|
|
2a13ca2fbf | ||
|
|
59e6afcaad | ||
|
|
4d9a455a27 | ||
|
|
74b2c79d46 | ||
|
|
ed407b091c | ||
|
|
6c8020a3b9 | ||
|
|
42f18d1e51 | ||
|
|
abf3e4ab50 | ||
|
|
d1078434af | ||
|
|
8f024dabc3 | ||
|
|
0c580c32c4 | ||
|
|
90960126e8 | ||
|
|
aa37f24fc1 | ||
|
|
3f4f287e6e | ||
|
|
55fe75c716 | ||
|
|
c6236ead67 | ||
|
|
68e3572278 |
8
.github/workflows/deploy.yml
vendored
8
.github/workflows/deploy.yml
vendored
@@ -17,13 +17,15 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
name: Deploy ${{ matrix.target }}
|
||||
|
||||
27
.github/workflows/main.yml
vendored
27
.github/workflows/main.yml
vendored
@@ -3,6 +3,9 @@ on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
env:
|
||||
BROWSER_UI_TEST_VERSION: '0.19.0'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -22,7 +25,7 @@ jobs:
|
||||
rust: nightly
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: stable x86_64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-musl
|
||||
- name: stable x86_64 macos
|
||||
@@ -38,9 +41,9 @@ jobs:
|
||||
rust: stable
|
||||
target: x86_64-pc-windows-msvc
|
||||
- name: msrv
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-22.04
|
||||
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
|
||||
rust: 1.74.0
|
||||
rust: 1.77.0
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: ${{ matrix.name }}
|
||||
steps:
|
||||
@@ -53,7 +56,7 @@ jobs:
|
||||
run: cargo test --no-default-features --target ${{ matrix.target }}
|
||||
|
||||
aarch64-cross-builds:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
@@ -70,6 +73,22 @@ jobs:
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt --check
|
||||
|
||||
gui:
|
||||
name: GUI tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- name: Install npm
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install browser-ui-test
|
||||
run: npm install browser-ui-test@"${BROWSER_UI_TEST_VERSION}"
|
||||
- name: Build and run tests (+ GUI)
|
||||
run: cargo test --locked --target x86_64-unknown-linux-gnu --test gui
|
||||
|
||||
# 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
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -16,3 +16,8 @@ test_book/book/
|
||||
# Ignore Vim temporary and swap files.
|
||||
*.sw?
|
||||
*~
|
||||
|
||||
# GUI tests
|
||||
node_modules
|
||||
package-lock.json
|
||||
package.json
|
||||
|
||||
90
CHANGELOG.md
90
CHANGELOG.md
@@ -1,8 +1,98 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.4.47
|
||||
[v0.4.46...v0.4.47](https://github.com/rust-lang/mdBook/compare/v0.4.46...v0.4.47)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed search not showing up in sub-directories.
|
||||
[#2586](https://github.com/rust-lang/mdBook/pull/2586)
|
||||
|
||||
## mdBook 0.4.46
|
||||
[v0.4.45...v0.4.46](https://github.com/rust-lang/mdBook/compare/v0.4.45...v0.4.46)
|
||||
|
||||
### Changed
|
||||
|
||||
- The `output.html.hash-files` config option has been added to add hashes to static filenames to bust any caches when a book is updated. `{{resource}}` template tags have been added so that links can be properly generated to those files.
|
||||
[#1368](https://github.com/rust-lang/mdBook/pull/1368)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Playground links for Rust 2024 now set the edition correctly.
|
||||
[#2557](https://github.com/rust-lang/mdBook/pull/2557)
|
||||
|
||||
## mdBook 0.4.45
|
||||
[v0.4.44...v0.4.45](https://github.com/rust-lang/mdBook/compare/v0.4.44...v0.4.45)
|
||||
|
||||
### Changed
|
||||
|
||||
- Added context to error message when rustdoc is not found.
|
||||
[#2545](https://github.com/rust-lang/mdBook/pull/2545)
|
||||
- Slightly changed the styling rules around margins of footnotes.
|
||||
[#2524](https://github.com/rust-lang/mdBook/pull/2524)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where it would panic if a source_path is not set.
|
||||
[#2550](https://github.com/rust-lang/mdBook/pull/2550)
|
||||
|
||||
## mdBook 0.4.44
|
||||
[v0.4.43...v0.4.44](https://github.com/rust-lang/mdBook/compare/v0.4.43...v0.4.44)
|
||||
|
||||
### Added
|
||||
|
||||
- Added pre-built aarch64-apple-darwin binaries to the releases.
|
||||
[#2500](https://github.com/rust-lang/mdBook/pull/2500)
|
||||
- `mdbook clean` now shows a summary of what it did.
|
||||
[#2458](https://github.com/rust-lang/mdBook/pull/2458)
|
||||
- Added the `output.html.search.chapter` config setting to disable search indexing of individual chapters.
|
||||
[#2533](https://github.com/rust-lang/mdBook/pull/2533)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed auto-scrolling the side-bar when loading a page with a `#` fragment URL.
|
||||
[#2517](https://github.com/rust-lang/mdBook/pull/2517)
|
||||
- Fixed display of sidebar when javascript is disabled.
|
||||
[#2529](https://github.com/rust-lang/mdBook/pull/2529)
|
||||
- Fixed the sidebar visibility getting out of sync with the button.
|
||||
[#2532](https://github.com/rust-lang/mdBook/pull/2532)
|
||||
|
||||
### Changed
|
||||
|
||||
- ❗ Rust code block hidden lines now follow the same logic as rustdoc. This requires a space after the `#` symbol.
|
||||
[#2530](https://github.com/rust-lang/mdBook/pull/2530)
|
||||
- ❗ Updated the Linux pre-built binaries which requires a newer version of glibc (2.34).
|
||||
[#2523](https://github.com/rust-lang/mdBook/pull/2523)
|
||||
- Updated dependencies
|
||||
[#2538](https://github.com/rust-lang/mdBook/pull/2538)
|
||||
[#2539](https://github.com/rust-lang/mdBook/pull/2539)
|
||||
|
||||
## mdBook 0.4.43
|
||||
[v0.4.42...v0.4.43](https://github.com/rust-lang/mdBook/compare/v0.4.42...v0.4.43)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed setting the title in `mdbook init` when no git user is configured.
|
||||
[#2486](https://github.com/rust-lang/mdBook/pull/2486)
|
||||
|
||||
### Changed
|
||||
|
||||
- The Rust 2024 edition no longer needs `-Zunstable-options`.
|
||||
[#2495](https://github.com/rust-lang/mdBook/pull/2495)
|
||||
|
||||
## mdBook 0.4.42
|
||||
[v0.4.41...v0.4.42](https://github.com/rust-lang/mdBook/compare/v0.4.41...v0.4.42)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed chapter list folding.
|
||||
[#2473](https://github.com/rust-lang/mdBook/pull/2473)
|
||||
|
||||
## mdBook 0.4.41
|
||||
[v0.4.40...v0.4.41](https://github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)
|
||||
|
||||
**Note:** If you have a custom `index.hbs` theme file, you will need to update it to the latest version.
|
||||
|
||||
### Added
|
||||
|
||||
- Added preliminary support for Rust 2024 edition.
|
||||
|
||||
@@ -138,8 +138,23 @@ We generally strive to keep mdBook compatible with a relatively recent browser o
|
||||
That is, supporting Chrome, Safari, Firefox, Edge on Windows, macOS, Linux, iOS, and Android.
|
||||
If possible, do your best to avoid breaking older browser releases.
|
||||
|
||||
Any change to the HTML or styling is encouraged to manually check on as many browsers and platforms that you can.
|
||||
Unfortunately at this time we don't have any automated UI or browser testing, so your assistance in testing is appreciated.
|
||||
GUI tests are checked with the GUI testsuite. To run it, you need to install `npm` first. Then run:
|
||||
|
||||
```
|
||||
cargo test --test gui
|
||||
```
|
||||
|
||||
The first time, it'll fail and ask you to install the `browser-ui-test` package. Install it then re-run the tests.
|
||||
|
||||
If you want to disable the headless mode, use the `DISABLE_HEADLESS_TEST=1` environment variable:
|
||||
|
||||
```
|
||||
cargo test --test gui -- --disable-headless-test
|
||||
```
|
||||
|
||||
The GUI tests are in the directory `tests/gui` in text files with the `.goml` extension. These tests are run
|
||||
using a `node.js` framework called `browser-ui-test`. You can find documentation for this language on its
|
||||
[repository](https://github.com/GuillaumeGomez/browser-UI-test/blob/master/goml-script.md).
|
||||
|
||||
## Updating highlight.js
|
||||
|
||||
|
||||
681
Cargo.lock
generated
681
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@@ -3,7 +3,7 @@ members = [".", "examples/remove-emphasis/mdbook-remove-emphasis"]
|
||||
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.41"
|
||||
version = "0.4.47"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
@@ -17,7 +17,7 @@ license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
description = "Creates a book from markdown files"
|
||||
rust-version = "1.74"
|
||||
rust-version = "1.77" # Keep in sync with installation.md and .github/workflows/main.yml
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
@@ -27,6 +27,7 @@ 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"
|
||||
@@ -34,14 +35,15 @@ pulldown-cmark = { version = "0.10.0", default-features = false, features = ["ht
|
||||
regex = "1.8.1"
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
sha2 = "0.10.8"
|
||||
shlex = "1.3.0"
|
||||
tempfile = "3.4.0"
|
||||
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
|
||||
topological-sort = "0.2.2"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "6.1.1", optional = true }
|
||||
notify-debouncer-mini = { version = "0.4.1", optional = true }
|
||||
notify = { version = "8.0.0", optional = true }
|
||||
notify-debouncer-mini = { version = "0.6.0", optional = true }
|
||||
ignore = { version = "0.4.20", optional = true }
|
||||
pathdiff = { version = "0.2.1", optional = true }
|
||||
walkdir = { version = "2.3.3", optional = true }
|
||||
@@ -82,3 +84,10 @@ name = "remove-emphasis"
|
||||
path = "examples/remove-emphasis/test.rs"
|
||||
crate-type = ["lib"]
|
||||
test = true
|
||||
|
||||
[[test]]
|
||||
harness = false
|
||||
test = false
|
||||
name = "gui"
|
||||
path = "tests/gui/runner.rs"
|
||||
crate-type = ["bin"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# mdBook
|
||||
|
||||
[](https://github.com/rust-lang/mdBook/actions?workflow=CI)
|
||||
[](https://github.com/rust-lang/mdBook/actions/workflows/main.yml)
|
||||
[](https://crates.io/crates/mdbook)
|
||||
[](LICENSE)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ mathjax-support = true
|
||||
site-url = "/mdBook/"
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
|
||||
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
hash-files = true
|
||||
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
|
||||
@@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex
|
||||
|
||||
```sh
|
||||
mkdir bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.41/mdbook-v0.4.41-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.47/mdbook-v0.4.47-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
bin/mdbook build
|
||||
```
|
||||
|
||||
|
||||
@@ -168,6 +168,12 @@ The following configuration options are available:
|
||||
This string will be written to a file named CNAME in the root of your site, as
|
||||
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages
|
||||
site*][custom domain]).
|
||||
- **hash-files:** Include a cryptographic "fingerprint" of the files' contents in static asset filenames,
|
||||
so that if the contents of the file are changed, the name of the file will also change.
|
||||
For example, `css/chrome.css` may become `css/chrome-9b8f428e.css`.
|
||||
Chapter HTML files are not renamed.
|
||||
Static CSS and JS files can reference each other using `{{ resource "filename" }}` directives.
|
||||
Defaults to `false` (in a future release, this may change to `true`).
|
||||
|
||||
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||
|
||||
@@ -281,6 +287,20 @@ copy-js = true # include Javascript code for search
|
||||
- **copy-js:** Copy JavaScript files for the search implementation to the output
|
||||
directory. Defaults to `true`.
|
||||
|
||||
#### `[output.html.search.chapter]`
|
||||
|
||||
The [`output.html.search.chapter`] table provides the ability to modify search settings per chapter or directory. Each key is the path to the chapter source file or directory, and the value is a table of settings to apply to that path. This will merge recursively, with more specific paths taking precedence.
|
||||
|
||||
```toml
|
||||
[output.html.search.chapter]
|
||||
# Disables search indexing for all chapters in the `appendix` directory.
|
||||
"appendix" = { enable = false }
|
||||
# Enables search indexing for just this one appendix chapter.
|
||||
"appendix/glossary.md" = { enable = true }
|
||||
```
|
||||
|
||||
- **enable:** Enables or disables search indexing for the given chapters. Defaults to `true`. This does not override the overall `output.html.search.enable` setting; that must be `true` for any search functionality to be enabled. Be cautious when disabling indexing for chapters because that can potentially lead to user confusion when they search for terms and expect them to be found. This should only be used in exceptional circumstances where keeping the chapter in the index will cause issues with the quality of the search results.
|
||||
|
||||
### `[output.html.redirect]`
|
||||
|
||||
The `[output.html.redirect]` table provides a way to add redirects.
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them with a specific prefix.
|
||||
|
||||
For the Rust language, you can use the `#` character as a prefix which will hide lines [like you would with Rustdoc][rustdoc-hide].
|
||||
For the Rust language, you can prefix lines with `# ` (`#` followed by a space) to hide them [like you would with Rustdoc][rustdoc-hide].
|
||||
This prefix can be escaped with `##` to prevent the hiding of a line that should begin with the literal string `# ` (see [Rustdoc's docs][rustdoc-hide] for more details)
|
||||
|
||||
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/write-documentation/documentation-tests.html#hiding-portions-of-the-example
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ be added to the ***book.toml***:
|
||||
editable = true
|
||||
```
|
||||
|
||||
To make a specific block available for editing, the attribute `editable` needs
|
||||
to be added to it:
|
||||
After enabling editable code blocks, the `editable` attribute must be added to a
|
||||
code block to make it editable:
|
||||
|
||||
~~~markdown
|
||||
```rust,editable
|
||||
|
||||
@@ -99,3 +99,13 @@ Of course the inner html can be changed to your liking.
|
||||
|
||||
*If you would like other properties or helpers exposed, please [create a new
|
||||
issue](https://github.com/rust-lang/mdBook/issues)*
|
||||
|
||||
### 3. resource
|
||||
|
||||
The path to a static file.
|
||||
It implicitly includes `path_to_root`,
|
||||
and accounts for files that are renamed with a hash in their filename.
|
||||
|
||||
```handlebars
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ To make it easier to run, put the path to the binary into your `PATH`.
|
||||
|
||||
To build the `mdbook` executable from source, you will first need to install Rust and Cargo.
|
||||
Follow the instructions on the [Rust installation page].
|
||||
mdBook currently requires at least Rust version 1.74.
|
||||
mdBook currently requires at least Rust version 1.77.
|
||||
|
||||
Once you have installed Rust, the following command can be used to build and install mdBook:
|
||||
|
||||
|
||||
@@ -173,7 +173,8 @@ pub struct Chapter {
|
||||
/// `index.md` via the [`Chapter::path`] field. The `source_path` field
|
||||
/// exists if you need access to the true file path.
|
||||
///
|
||||
/// This is `None` for a draft chapter.
|
||||
/// This is `None` for a draft chapter, or a synthetically generated
|
||||
/// chapter that has no file on disk.
|
||||
pub source_path: Option<PathBuf>,
|
||||
/// An ordered list of the names of each chapter above this one in the hierarchy.
|
||||
pub parent_names: Vec<String>,
|
||||
|
||||
@@ -346,7 +346,7 @@ impl MDBook {
|
||||
cmd.args(["--edition", "2021"]);
|
||||
}
|
||||
RustEdition::E2024 => {
|
||||
cmd.args(["--edition", "2024", "-Zunstable-options"]);
|
||||
cmd.args(["--edition", "2024"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,7 +356,9 @@ impl MDBook {
|
||||
}
|
||||
|
||||
debug!("running {:?}", cmd);
|
||||
let output = cmd.output()?;
|
||||
let output = cmd
|
||||
.output()
|
||||
.with_context(|| "failed to execute `rustdoc`")?;
|
||||
|
||||
if !output.status.success() {
|
||||
failed = true;
|
||||
|
||||
@@ -747,6 +747,20 @@ mod tests {
|
||||
let got = parser.parse_affix(false);
|
||||
|
||||
assert!(got.is_err());
|
||||
let error_message = got.err().unwrap().to_string();
|
||||
assert_eq!(error_message, "failed to parse SUMMARY.md line 2, column 1: Suffix chapters cannot be followed by a list");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expected_a_start_of_a_link() {
|
||||
let src = "- Title\n";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
let got = parser.parse_affix(false);
|
||||
|
||||
assert!(got.is_err());
|
||||
let error_message = got.err().unwrap().to_string();
|
||||
assert_eq!(error_message, "failed to parse SUMMARY.md line 1, column 0: Suffix chapters cannot be followed by a list");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,8 +2,9 @@ use super::command_prelude::*;
|
||||
use crate::get_book_dir;
|
||||
use anyhow::Context;
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
use std::mem::take;
|
||||
use std::path::PathBuf;
|
||||
use std::{fmt, fs};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand() -> Command {
|
||||
@@ -23,10 +24,88 @@ pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
None => book.root.join(&book.config.build.build_dir),
|
||||
};
|
||||
|
||||
if dir_to_remove.exists() {
|
||||
fs::remove_dir_all(&dir_to_remove)
|
||||
.with_context(|| "Unable to remove the build directory")?;
|
||||
}
|
||||
let removed = Clean::new(&dir_to_remove)?;
|
||||
println!("{removed}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Formats a number of bytes into a human readable SI-prefixed size.
|
||||
/// Returns a tuple of `(quantity, units)`.
|
||||
pub fn human_readable_bytes(bytes: u64) -> (f32, &'static str) {
|
||||
static UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
||||
let bytes = bytes as f32;
|
||||
let i = ((bytes.log2() / 10.0) as usize).min(UNITS.len() - 1);
|
||||
(bytes / 1024_f32.powi(i as i32), UNITS[i])
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Clean {
|
||||
num_files_removed: u64,
|
||||
num_dirs_removed: u64,
|
||||
total_bytes_removed: u64,
|
||||
}
|
||||
|
||||
impl Clean {
|
||||
fn new(dir: &PathBuf) -> mdbook::errors::Result<Clean> {
|
||||
let mut files = vec![dir.clone()];
|
||||
let mut children = Vec::new();
|
||||
let mut num_files_removed = 0;
|
||||
let mut num_dirs_removed = 0;
|
||||
let mut total_bytes_removed = 0;
|
||||
|
||||
if dir.exists() {
|
||||
while !files.is_empty() {
|
||||
for file in files {
|
||||
if let Ok(meta) = file.metadata() {
|
||||
// Note: This can over-count bytes removed for hard-linked
|
||||
// files. It also under-counts since it only counts the exact
|
||||
// byte sizes and not the block sizes.
|
||||
total_bytes_removed += meta.len();
|
||||
}
|
||||
if file.is_file() {
|
||||
num_files_removed += 1;
|
||||
} else if file.is_dir() {
|
||||
num_dirs_removed += 1;
|
||||
for entry in fs::read_dir(file)? {
|
||||
children.push(entry?.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
files = take(&mut children);
|
||||
}
|
||||
fs::remove_dir_all(&dir).with_context(|| "Unable to remove the build directory")?;
|
||||
}
|
||||
|
||||
Ok(Clean {
|
||||
num_files_removed,
|
||||
num_dirs_removed,
|
||||
total_bytes_removed,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Clean {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Removed ")?;
|
||||
match (self.num_files_removed, self.num_dirs_removed) {
|
||||
(0, 0) => write!(f, "0 files")?,
|
||||
(0, 1) => write!(f, "1 directory")?,
|
||||
(0, 2..) => write!(f, "{} directories", self.num_dirs_removed)?,
|
||||
(1, _) => write!(f, "1 file")?,
|
||||
(2.., _) => write!(f, "{} files", self.num_files_removed)?,
|
||||
}
|
||||
|
||||
if self.total_bytes_removed == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
// Don't show a fractional number of bytes.
|
||||
if self.total_bytes_removed < 1024 {
|
||||
write!(f, ", {}B total", self.total_bytes_removed)
|
||||
} else {
|
||||
let (bytes, unit) = human_readable_bytes(self.total_bytes_removed);
|
||||
write!(f, ", {bytes:.2}{unit} total")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
if let Some(author) = get_author_name() {
|
||||
debug!("Obtained user name from gitconfig: {:?}", author);
|
||||
config.book.authors.push(author);
|
||||
builder.with_config(config);
|
||||
}
|
||||
|
||||
builder.with_config(config);
|
||||
builder.build()?;
|
||||
println!("\nAll done, no errors...");
|
||||
|
||||
|
||||
@@ -587,6 +587,9 @@ pub struct HtmlConfig {
|
||||
/// The mapping from old pages to new pages/URLs to use when generating
|
||||
/// redirects.
|
||||
pub redirect: HashMap<String, String>,
|
||||
/// If this option is turned on, "cache bust" static files by adding
|
||||
/// hashes to their file names.
|
||||
pub hash_files: bool,
|
||||
}
|
||||
|
||||
impl Default for HtmlConfig {
|
||||
@@ -616,6 +619,7 @@ impl Default for HtmlConfig {
|
||||
cname: None,
|
||||
live_reload_endpoint: None,
|
||||
redirect: HashMap::new(),
|
||||
hash_files: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -735,6 +739,11 @@ pub struct Search {
|
||||
/// Copy JavaScript files for the search functionality to the output directory?
|
||||
/// Default: `true`.
|
||||
pub copy_js: bool,
|
||||
/// Specifies search settings for the given path.
|
||||
///
|
||||
/// The path can be for a specific chapter, or a directory. This will
|
||||
/// merge recursively, with more specific paths taking precedence.
|
||||
pub chapter: HashMap<String, SearchChapterSettings>,
|
||||
}
|
||||
|
||||
impl Default for Search {
|
||||
@@ -751,10 +760,19 @@ impl Default for Search {
|
||||
expand: true,
|
||||
heading_split_level: 3,
|
||||
copy_js: true,
|
||||
chapter: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Search options for chapters (or paths).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct SearchChapterSettings {
|
||||
/// Whether or not indexing is enabled, default `true`.
|
||||
pub enable: Option<bool>,
|
||||
}
|
||||
|
||||
/// Allows you to "update" any arbitrary field in a struct by round-tripping via
|
||||
/// a `toml::Value`.
|
||||
///
|
||||
|
||||
@@ -19,9 +19,9 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
|
||||
///
|
||||
/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
|
||||
///. lines, or only between the specified anchors.
|
||||
/// lines, or only between the specified anchors.
|
||||
/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
|
||||
///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
|
||||
/// specified or the lines between specified anchors, and include the rest of the file behind `#`.
|
||||
/// This hides the lines from initial display but shows them when the reader expands the code
|
||||
/// block and provides them to Rustdoc for testing.
|
||||
/// - `{{# playground}}` - Insert runnable Rust files
|
||||
|
||||
@@ -2,8 +2,9 @@ use crate::book::{Book, BookItem};
|
||||
use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers;
|
||||
use crate::renderer::html_handlebars::StaticFiles;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::theme::{self, playground_editor, Theme};
|
||||
use crate::theme::{self, Theme};
|
||||
use crate::utils;
|
||||
|
||||
use std::borrow::Cow;
|
||||
@@ -222,134 +223,6 @@ impl HtmlHandlebars {
|
||||
rendered
|
||||
}
|
||||
|
||||
fn copy_static_files(
|
||||
&self,
|
||||
destination: &Path,
|
||||
theme: &Theme,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<()> {
|
||||
use crate::utils::fs::write_file;
|
||||
|
||||
write_file(
|
||||
destination,
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
|
||||
}
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
write_file(destination, "css/general.css", &theme.general_css)?;
|
||||
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
|
||||
if html_config.print.enable {
|
||||
write_file(destination, "css/print.css", &theme.print_css)?;
|
||||
}
|
||||
write_file(destination, "css/variables.css", &theme.variables_css)?;
|
||||
if let Some(contents) = &theme.favicon_png {
|
||||
write_file(destination, "favicon.png", contents)?;
|
||||
}
|
||||
if let Some(contents) = &theme.favicon_svg {
|
||||
write_file(destination, "favicon.svg", contents)?;
|
||||
}
|
||||
write_file(destination, "highlight.css", &theme.highlight_css)?;
|
||||
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
write_file(destination, "highlight.js", &theme.highlight_js)?;
|
||||
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/css/font-awesome.css",
|
||||
theme::FONT_AWESOME,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
// Don't copy the stock fonts if the user has specified their own fonts to use.
|
||||
if html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
}
|
||||
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
}
|
||||
write_file(
|
||||
destination,
|
||||
theme::fonts::SOURCE_CODE_PRO.0,
|
||||
theme::fonts::SOURCE_CODE_PRO.1,
|
||||
)?;
|
||||
}
|
||||
if let Some(fonts_css) = &theme.fonts_css {
|
||||
if !fonts_css.is_empty() {
|
||||
write_file(destination, "fonts/fonts.css", fonts_css)?;
|
||||
}
|
||||
}
|
||||
if !html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
warn!(
|
||||
"output.html.copy-fonts is deprecated.\n\
|
||||
This book appears to have copy-fonts=false in book.toml without a fonts.css file.\n\
|
||||
Add an empty `theme/fonts/fonts.css` file to squelch this warning."
|
||||
);
|
||||
}
|
||||
for font_file in &theme.font_files {
|
||||
let contents = fs::read(font_file)?;
|
||||
let filename = font_file.file_name().unwrap();
|
||||
let filename = Path::new("fonts").join(filename);
|
||||
write_file(destination, filename, &contents)?;
|
||||
}
|
||||
|
||||
let playground_config = &html_config.playground;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playground_config.editable && playground_config.copy_js {
|
||||
// Load the editor
|
||||
write_file(destination, "editor.js", playground_editor::JS)?;
|
||||
write_file(destination, "ace.js", playground_editor::ACE_JS)?;
|
||||
write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-dawn.js",
|
||||
playground_editor::THEME_DAWN_JS,
|
||||
)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-tomorrow_night.js",
|
||||
playground_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the context with data for this file
|
||||
fn configure_print_version(
|
||||
&self,
|
||||
@@ -381,43 +254,6 @@ impl HtmlHandlebars {
|
||||
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
|
||||
}
|
||||
|
||||
/// Copy across any additional CSS and JavaScript files which the book
|
||||
/// has been configured to use.
|
||||
fn copy_additional_css_and_js(
|
||||
&self,
|
||||
html: &HtmlConfig,
|
||||
root: &Path,
|
||||
destination: &Path,
|
||||
) -> Result<()> {
|
||||
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
|
||||
|
||||
debug!("Copying additional CSS and JS");
|
||||
|
||||
for custom_file in custom_files {
|
||||
let input_location = root.join(custom_file);
|
||||
let output_location = destination.join(custom_file);
|
||||
if let Some(parent) = output_location.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Unable to create {}", parent.display()))?;
|
||||
}
|
||||
debug!(
|
||||
"Copying {} -> {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
);
|
||||
|
||||
fs::copy(&input_location, &output_location).with_context(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirects(
|
||||
&self,
|
||||
root: &Path,
|
||||
@@ -544,6 +380,57 @@ impl Renderer for HtmlHandlebars {
|
||||
fs::create_dir_all(destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
let mut static_files = StaticFiles::new(&theme, &html_config, &ctx.root)?;
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
{
|
||||
let default = crate::config::Search::default();
|
||||
let search = html_config.search.as_ref().unwrap_or(&default);
|
||||
if search.enable {
|
||||
super::search::create_files(&search, &mut static_files, &book)?;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Render toc js");
|
||||
{
|
||||
let rendered_toc = handlebars.render("toc_js", &data)?;
|
||||
static_files.add_builtin("toc.js", rendered_toc.as_bytes());
|
||||
debug!("Creating toc.js ✓");
|
||||
}
|
||||
|
||||
if html_config.hash_files {
|
||||
static_files.hash_files()?;
|
||||
}
|
||||
|
||||
debug!("Copy static files");
|
||||
let resource_helper = static_files
|
||||
.write_files(&destination)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
|
||||
handlebars.register_helper("resource", Box::new(resource_helper));
|
||||
|
||||
debug!("Render toc html");
|
||||
{
|
||||
data.insert("is_toc_html".to_owned(), json!(true));
|
||||
data.insert("path".to_owned(), json!("toc.html"));
|
||||
let rendered_toc = handlebars.render("toc_html", &data)?;
|
||||
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.html ✓");
|
||||
data.remove("path");
|
||||
data.remove("is_toc_html");
|
||||
}
|
||||
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
utils::fs::write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
|
||||
}
|
||||
|
||||
let mut is_index = true;
|
||||
for item in book.iter() {
|
||||
let ctx = RenderItemContext {
|
||||
@@ -588,33 +475,6 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Creating print.html ✓");
|
||||
}
|
||||
|
||||
debug!("Render toc");
|
||||
{
|
||||
let rendered_toc = handlebars.render("toc_js", &data)?;
|
||||
utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.js ✓");
|
||||
data.insert("is_toc_html".to_owned(), json!(true));
|
||||
let rendered_toc = handlebars.render("toc_html", &data)?;
|
||||
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.html ✓");
|
||||
data.remove("is_toc_html");
|
||||
}
|
||||
|
||||
debug!("Copy static files");
|
||||
self.copy_static_files(destination, &theme, &html_config)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
self.copy_additional_css_and_js(&html_config, &ctx.root, destination)
|
||||
.with_context(|| "Unable to copy across additional CSS and JS")?;
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
{
|
||||
let search = html_config.search.unwrap_or_default();
|
||||
if search.enable {
|
||||
super::search::create_files(&search, destination, book)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
|
||||
.context("Unable to emit redirects")?;
|
||||
|
||||
@@ -931,7 +791,7 @@ fn add_playground_pre(
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!("# #![allow(unused)]\n{attrs}#fn main() {{\n{code}#}}").into()
|
||||
format!("# #![allow(unused)]\n{attrs}# fn main() {{\n{code}# }}").into()
|
||||
};
|
||||
content
|
||||
}
|
||||
@@ -1003,12 +863,9 @@ fn hide_lines_rust(content: &str) -> String {
|
||||
result += &caps[3];
|
||||
result += newline;
|
||||
continue;
|
||||
} else if &caps[2] != "!" && &caps[2] != "[" {
|
||||
} else if matches!(&caps[2], "" | " ") {
|
||||
result += "<span class=\"boring\">";
|
||||
result += &caps[1];
|
||||
if &caps[2] != " " {
|
||||
result += &caps[2];
|
||||
}
|
||||
result += &caps[3];
|
||||
result += newline;
|
||||
result += "</span>";
|
||||
@@ -1134,7 +991,7 @@ mod tests {
|
||||
fn add_playground() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
|
||||
@@ -1164,7 +1021,7 @@ mod tests {
|
||||
fn add_playground_edition2015() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
@@ -1188,7 +1045,7 @@ mod tests {
|
||||
fn add_playground_edition2018() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
@@ -1212,7 +1069,7 @@ mod tests {
|
||||
fn add_playground_edition2021() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
@@ -1237,8 +1094,12 @@ mod tests {
|
||||
fn hide_lines_language_rust() {
|
||||
let inputs = [
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n# #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n# #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>",),
|
||||
// # must be followed by a space for a line to be hidden
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n#fn main() {\nx()\n#}</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n#fn main() {\nx()\n#}</code></pre>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod navigation;
|
||||
pub mod resources;
|
||||
pub mod theme;
|
||||
pub mod toc;
|
||||
|
||||
50
src/renderer/html_handlebars/helpers/resources.rs
Normal file
50
src/renderer/html_handlebars/helpers/resources.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::utils;
|
||||
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
};
|
||||
|
||||
// Handlebars helper to find filenames with hashes in them
|
||||
#[derive(Clone)]
|
||||
pub struct ResourceHelper {
|
||||
pub hash_map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl HelperDef for ResourceHelper {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
h: &Helper<'rc>,
|
||||
_r: &'reg Handlebars<'_>,
|
||||
ctx: &'rc Context,
|
||||
rc: &mut RenderContext<'reg, 'rc>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
|
||||
RenderErrorReason::Other(
|
||||
"Param 0 with String type is required for theme_option helper.".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let base_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||
})?
|
||||
.replace("\"", "");
|
||||
|
||||
let path_to_root = utils::fs::path_to_root(&base_path);
|
||||
|
||||
out.write(&path_to_root)?;
|
||||
out.write(
|
||||
self.hash_map
|
||||
.get(¶m[..])
|
||||
.map(|p| &p[..])
|
||||
.unwrap_or(¶m),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
#![allow(missing_docs)] // FIXME: Document this
|
||||
|
||||
pub use self::hbs_renderer::HtmlHandlebars;
|
||||
pub use self::static_files::StaticFiles;
|
||||
|
||||
mod hbs_renderer;
|
||||
mod helpers;
|
||||
mod static_files;
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
mod search;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use elasticlunr::{Index, IndexBuilder};
|
||||
use once_cell::sync::Lazy;
|
||||
use pulldown_cmark::*;
|
||||
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::Search;
|
||||
use crate::book::{Book, BookItem, Chapter};
|
||||
use crate::config::{Search, SearchChapterSettings};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::StaticFiles;
|
||||
use crate::theme::searcher;
|
||||
use crate::utils;
|
||||
use log::{debug, warn};
|
||||
@@ -26,7 +27,11 @@ fn tokenize(text: &str) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// Creates all files required for search.
|
||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
||||
pub fn create_files(
|
||||
search_config: &Search,
|
||||
static_files: &mut StaticFiles,
|
||||
book: &Book,
|
||||
) -> Result<()> {
|
||||
let mut index = IndexBuilder::new()
|
||||
.add_field_with_tokenizer("title", Box::new(&tokenize))
|
||||
.add_field_with_tokenizer("body", Box::new(&tokenize))
|
||||
@@ -35,8 +40,21 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
|
||||
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
||||
|
||||
let chapter_configs = sort_search_config(&search_config.chapter);
|
||||
validate_chapter_config(&chapter_configs, book)?;
|
||||
|
||||
for item in book.iter() {
|
||||
render_item(&mut index, search_config, &mut doc_urls, item)?;
|
||||
let chapter = match item {
|
||||
BookItem::Chapter(ch) if !ch.is_draft_chapter() => ch,
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(path) = settings_path(chapter) {
|
||||
let chapter_settings = get_chapter_settings(&chapter_configs, path);
|
||||
if !chapter_settings.enable.unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
render_item(&mut index, search_config, &mut doc_urls, chapter)?;
|
||||
}
|
||||
|
||||
let index = write_to_json(index, search_config, doc_urls)?;
|
||||
@@ -46,15 +64,14 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
}
|
||||
|
||||
if search_config.copy_js {
|
||||
utils::fs::write_file(destination, "searchindex.json", index.as_bytes())?;
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
static_files.add_builtin("searchindex.json", index.as_bytes());
|
||||
static_files.add_builtin(
|
||||
"searchindex.js",
|
||||
format!("Object.assign(window.search, {index});").as_bytes(),
|
||||
)?;
|
||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
|
||||
format!("Object.assign(window.search, {});", index).as_bytes(),
|
||||
);
|
||||
static_files.add_builtin("searcher.js", searcher::JS);
|
||||
static_files.add_builtin("mark.min.js", searcher::MARK_JS);
|
||||
static_files.add_builtin("elasticlunr.min.js", searcher::ELASTICLUNR_JS);
|
||||
debug!("Copying search files ✓");
|
||||
}
|
||||
|
||||
@@ -100,13 +117,8 @@ fn render_item(
|
||||
index: &mut Index,
|
||||
search_config: &Search,
|
||||
doc_urls: &mut Vec<String>,
|
||||
item: &BookItem,
|
||||
chapter: &Chapter,
|
||||
) -> Result<()> {
|
||||
let chapter = match *item {
|
||||
BookItem::Chapter(ref ch) if !ch.is_draft_chapter() => ch,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let chapter_path = chapter
|
||||
.path
|
||||
.as_ref()
|
||||
@@ -313,3 +325,82 @@ fn clean_html(html: &str) -> String {
|
||||
});
|
||||
AMMONIA.clean(html).to_string()
|
||||
}
|
||||
|
||||
fn settings_path(ch: &Chapter) -> Option<&Path> {
|
||||
ch.source_path.as_deref().or_else(|| ch.path.as_deref())
|
||||
}
|
||||
|
||||
fn validate_chapter_config(
|
||||
chapter_configs: &[(PathBuf, SearchChapterSettings)],
|
||||
book: &Book,
|
||||
) -> Result<()> {
|
||||
for (path, _) in chapter_configs {
|
||||
let found = book
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
BookItem::Chapter(ch) if !ch.is_draft_chapter() => settings_path(ch),
|
||||
_ => None,
|
||||
})
|
||||
.any(|source_path| source_path.starts_with(path));
|
||||
if !found {
|
||||
bail!(
|
||||
"[output.html.search.chapter] key `{}` does not match any chapter paths",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sort_search_config(
|
||||
map: &HashMap<String, SearchChapterSettings>,
|
||||
) -> Vec<(PathBuf, SearchChapterSettings)> {
|
||||
let mut settings: Vec<_> = map
|
||||
.iter()
|
||||
.map(|(key, value)| (PathBuf::from(key), value.clone()))
|
||||
.collect();
|
||||
// Note: This is case-sensitive, and assumes the author uses the same case
|
||||
// as the actual filename.
|
||||
settings.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
settings
|
||||
}
|
||||
|
||||
fn get_chapter_settings(
|
||||
chapter_configs: &[(PathBuf, SearchChapterSettings)],
|
||||
source_path: &Path,
|
||||
) -> SearchChapterSettings {
|
||||
let mut result = SearchChapterSettings::default();
|
||||
for (path, config) in chapter_configs {
|
||||
if source_path.starts_with(path) {
|
||||
result.enable = config.enable.or(result.enable);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chapter_settings_priority() {
|
||||
let cfg = r#"
|
||||
[output.html.search.chapter]
|
||||
"cli/watch.md" = { enable = true }
|
||||
"cli" = { enable = false }
|
||||
"cli/inner/foo.md" = { enable = false }
|
||||
"cli/inner" = { enable = true }
|
||||
"foo" = {} # Just to make sure empty table is allowed.
|
||||
"#;
|
||||
let cfg: crate::Config = toml::from_str(cfg).unwrap();
|
||||
let html = cfg.html_config().unwrap();
|
||||
let chapter_configs = sort_search_config(&html.search.unwrap().chapter);
|
||||
for (path, enable) in [
|
||||
("foo.md", None),
|
||||
("cli/watch.md", Some(true)),
|
||||
("cli/index.md", Some(false)),
|
||||
("cli/inner/index.md", Some(true)),
|
||||
("cli/inner/foo.md", Some(false)),
|
||||
] {
|
||||
assert_eq!(
|
||||
get_chapter_settings(&chapter_configs, Path::new(path)),
|
||||
SearchChapterSettings { enable }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
358
src/renderer/html_handlebars/static_files.rs
Normal file
358
src/renderer/html_handlebars/static_files.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
//! Support for writing static files.
|
||||
|
||||
use log::{debug, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::config::HtmlConfig;
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers::resources::ResourceHelper;
|
||||
use crate::theme::{self, playground_editor, Theme};
|
||||
use crate::utils;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Map static files to their final names and contents.
|
||||
///
|
||||
/// It performs [fingerprinting], if you call the `hash_files` method.
|
||||
/// If hash-files is turned off, then the files will not be renamed.
|
||||
/// It also writes files to their final destination, when `write_files` is called,
|
||||
/// and interprets the `{{ resource }}` directives to allow assets to name each other.
|
||||
///
|
||||
/// [fingerprinting]: https://guides.rubyonrails.org/asset_pipeline.html#fingerprinting-versioning-with-digest-based-urls
|
||||
pub struct StaticFiles {
|
||||
static_files: Vec<StaticFile>,
|
||||
hash_map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
enum StaticFile {
|
||||
Builtin {
|
||||
data: Vec<u8>,
|
||||
filename: String,
|
||||
},
|
||||
Additional {
|
||||
input_location: PathBuf,
|
||||
filename: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl StaticFiles {
|
||||
pub fn new(theme: &Theme, html_config: &HtmlConfig, root: &Path) -> Result<StaticFiles> {
|
||||
let static_files = Vec::new();
|
||||
let mut this = StaticFiles {
|
||||
hash_map: HashMap::new(),
|
||||
static_files,
|
||||
};
|
||||
|
||||
this.add_builtin("book.js", &theme.js);
|
||||
this.add_builtin("css/general.css", &theme.general_css);
|
||||
this.add_builtin("css/chrome.css", &theme.chrome_css);
|
||||
if html_config.print.enable {
|
||||
this.add_builtin("css/print.css", &theme.print_css);
|
||||
}
|
||||
this.add_builtin("css/variables.css", &theme.variables_css);
|
||||
if let Some(contents) = &theme.favicon_png {
|
||||
this.add_builtin("favicon.png", contents);
|
||||
}
|
||||
if let Some(contents) = &theme.favicon_svg {
|
||||
this.add_builtin("favicon.svg", contents);
|
||||
}
|
||||
this.add_builtin("highlight.css", &theme.highlight_css);
|
||||
this.add_builtin("tomorrow-night.css", &theme.tomorrow_night_css);
|
||||
this.add_builtin("ayu-highlight.css", &theme.ayu_highlight_css);
|
||||
this.add_builtin("highlight.js", &theme.highlight_js);
|
||||
this.add_builtin("clipboard.min.js", &theme.clipboard_js);
|
||||
this.add_builtin("FontAwesome/css/font-awesome.css", theme::FONT_AWESOME);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
);
|
||||
this.add_builtin(
|
||||
"FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
);
|
||||
this.add_builtin("FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF);
|
||||
if html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
this.add_builtin("fonts/fonts.css", theme::fonts::CSS);
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
this.add_builtin(file_name, contents);
|
||||
}
|
||||
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
|
||||
this.add_builtin(file_name, contents);
|
||||
}
|
||||
this.add_builtin(
|
||||
theme::fonts::SOURCE_CODE_PRO.0,
|
||||
theme::fonts::SOURCE_CODE_PRO.1,
|
||||
);
|
||||
} else if let Some(fonts_css) = &theme.fonts_css {
|
||||
if !fonts_css.is_empty() {
|
||||
this.add_builtin("fonts/fonts.css", fonts_css);
|
||||
}
|
||||
}
|
||||
if !html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
warn!(
|
||||
"output.html.copy-fonts is deprecated.\n\
|
||||
This book appears to have copy-fonts=false in book.toml without a fonts.css file.\n\
|
||||
Add an empty `theme/fonts/fonts.css` file to squelch this warning."
|
||||
);
|
||||
}
|
||||
|
||||
let playground_config = &html_config.playground;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playground_config.editable && playground_config.copy_js {
|
||||
// Load the editor
|
||||
this.add_builtin("editor.js", playground_editor::JS);
|
||||
this.add_builtin("ace.js", playground_editor::ACE_JS);
|
||||
this.add_builtin("mode-rust.js", playground_editor::MODE_RUST_JS);
|
||||
this.add_builtin("theme-dawn.js", playground_editor::THEME_DAWN_JS);
|
||||
this.add_builtin(
|
||||
"theme-tomorrow_night.js",
|
||||
playground_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
);
|
||||
}
|
||||
|
||||
let custom_files = html_config
|
||||
.additional_css
|
||||
.iter()
|
||||
.chain(html_config.additional_js.iter());
|
||||
|
||||
for custom_file in custom_files {
|
||||
let input_location = root.join(custom_file);
|
||||
|
||||
this.static_files.push(StaticFile::Additional {
|
||||
input_location,
|
||||
filename: custom_file
|
||||
.to_str()
|
||||
.with_context(|| "resource file names must be valid utf8")?
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
for input_location in theme.font_files.iter().cloned() {
|
||||
let filename = Path::new("fonts")
|
||||
.join(input_location.file_name().unwrap())
|
||||
.to_str()
|
||||
.with_context(|| "resource file names must be valid utf8")?
|
||||
.to_owned();
|
||||
this.static_files.push(StaticFile::Additional {
|
||||
input_location,
|
||||
filename,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
pub fn add_builtin(&mut self, filename: &str, data: &[u8]) {
|
||||
self.static_files.push(StaticFile::Builtin {
|
||||
filename: filename.to_owned(),
|
||||
data: data.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Updates this [`StaticFiles`] to hash the contents for determining the
|
||||
/// filename for each resource.
|
||||
pub fn hash_files(&mut self) -> Result<()> {
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::Read;
|
||||
for static_file in &mut self.static_files {
|
||||
match static_file {
|
||||
StaticFile::Builtin {
|
||||
ref mut filename,
|
||||
ref data,
|
||||
} => {
|
||||
let mut parts = filename.splitn(2, '.');
|
||||
let parts = parts.next().and_then(|p| Some((p, parts.next()?)));
|
||||
if let Some((name, suffix)) = parts {
|
||||
// FontAwesome already does its own cache busting with the ?v=4.7.0 thing,
|
||||
// and I don't want to have to patch its CSS file to use `{{ resource }}`
|
||||
if name != ""
|
||||
&& suffix != ""
|
||||
&& suffix != "txt"
|
||||
&& !name.starts_with("FontAwesome/fonts/")
|
||||
{
|
||||
let hex = hex::encode(&Sha256::digest(data)[..4]);
|
||||
let new_filename = format!("{}-{}.{}", name, hex, suffix);
|
||||
self.hash_map.insert(filename.clone(), new_filename.clone());
|
||||
*filename = new_filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
StaticFile::Additional {
|
||||
ref mut filename,
|
||||
ref input_location,
|
||||
} => {
|
||||
let mut parts = filename.splitn(2, '.');
|
||||
let parts = parts.next().and_then(|p| Some((p, parts.next()?)));
|
||||
if let Some((name, suffix)) = parts {
|
||||
if name != "" && suffix != "" {
|
||||
let mut digest = Sha256::new();
|
||||
let mut input_file = File::open(input_location)
|
||||
.with_context(|| "open static file for hashing")?;
|
||||
let mut buf = vec![0; 1024];
|
||||
loop {
|
||||
let amt = input_file
|
||||
.read(&mut buf)
|
||||
.with_context(|| "read static file for hashing")?;
|
||||
if amt == 0 {
|
||||
break;
|
||||
};
|
||||
digest.update(&buf[..amt]);
|
||||
}
|
||||
let hex = hex::encode(&digest.finalize()[..4]);
|
||||
let new_filename = format!("{}-{}.{}", name, hex, suffix);
|
||||
self.hash_map.insert(filename.clone(), new_filename.clone());
|
||||
*filename = new_filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_files(self, destination: &Path) -> Result<ResourceHelper> {
|
||||
use crate::utils::fs::write_file;
|
||||
use regex::bytes::{Captures, Regex};
|
||||
// The `{{ resource "name" }}` directive in static resources look like
|
||||
// handlebars syntax, even if they technically aren't.
|
||||
static RESOURCE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"\{\{ resource "([^"]+)" \}\}"#).unwrap());
|
||||
fn replace_all<'a>(
|
||||
hash_map: &HashMap<String, String>,
|
||||
data: &'a [u8],
|
||||
filename: &str,
|
||||
) -> Cow<'a, [u8]> {
|
||||
RESOURCE.replace_all(data, move |captures: &Captures<'_>| {
|
||||
let name = captures
|
||||
.get(1)
|
||||
.expect("capture 1 in resource regex")
|
||||
.as_bytes();
|
||||
let name = std::str::from_utf8(name).expect("resource name with invalid utf8");
|
||||
let resource_filename = hash_map.get(name).map(|s| &s[..]).unwrap_or(name);
|
||||
let path_to_root = utils::fs::path_to_root(filename);
|
||||
format!("{}{}", path_to_root, resource_filename)
|
||||
.as_bytes()
|
||||
.to_owned()
|
||||
})
|
||||
}
|
||||
for static_file in &self.static_files {
|
||||
match static_file {
|
||||
StaticFile::Builtin { filename, data } => {
|
||||
debug!("Writing builtin -> {}", filename);
|
||||
let data = if filename.ends_with(".css") || filename.ends_with(".js") {
|
||||
replace_all(&self.hash_map, data, filename)
|
||||
} else {
|
||||
Cow::Borrowed(&data[..])
|
||||
};
|
||||
write_file(destination, filename, &data)?;
|
||||
}
|
||||
StaticFile::Additional {
|
||||
ref input_location,
|
||||
ref filename,
|
||||
} => {
|
||||
let output_location = destination.join(filename);
|
||||
debug!(
|
||||
"Copying {} -> {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
);
|
||||
if let Some(parent) = output_location.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Unable to create {}", parent.display()))?;
|
||||
}
|
||||
if filename.ends_with(".css") || filename.ends_with(".js") {
|
||||
let data = fs::read(input_location)?;
|
||||
let data = replace_all(&self.hash_map, &data, filename);
|
||||
write_file(destination, filename, &data)?;
|
||||
} else {
|
||||
fs::copy(input_location, &output_location).with_context(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
input_location.display(),
|
||||
output_location.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hash_map = self.hash_map;
|
||||
Ok(ResourceHelper { hash_map })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::HtmlConfig;
|
||||
use crate::theme::Theme;
|
||||
use crate::utils::fs::write_file;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_write_directive() {
|
||||
let theme = Theme {
|
||||
index: Vec::new(),
|
||||
head: Vec::new(),
|
||||
redirect: Vec::new(),
|
||||
header: Vec::new(),
|
||||
chrome_css: Vec::new(),
|
||||
general_css: Vec::new(),
|
||||
print_css: Vec::new(),
|
||||
variables_css: Vec::new(),
|
||||
favicon_png: Some(Vec::new()),
|
||||
favicon_svg: Some(Vec::new()),
|
||||
js: Vec::new(),
|
||||
highlight_css: Vec::new(),
|
||||
tomorrow_night_css: Vec::new(),
|
||||
ayu_highlight_css: Vec::new(),
|
||||
highlight_js: Vec::new(),
|
||||
clipboard_js: Vec::new(),
|
||||
toc_js: Vec::new(),
|
||||
toc_html: Vec::new(),
|
||||
fonts_css: None,
|
||||
font_files: Vec::new(),
|
||||
};
|
||||
let temp_dir = TempDir::with_prefix("mdbook-").unwrap();
|
||||
let reference_js = Path::new("static-files-test-case-reference.js");
|
||||
let mut html_config = HtmlConfig::default();
|
||||
html_config.additional_js.push(reference_js.to_owned());
|
||||
write_file(
|
||||
temp_dir.path(),
|
||||
reference_js,
|
||||
br#"{{ resource "book.js" }}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut static_files = StaticFiles::new(&theme, &html_config, temp_dir.path()).unwrap();
|
||||
static_files.hash_files().unwrap();
|
||||
static_files.write_files(temp_dir.path()).unwrap();
|
||||
// custom JS winds up referencing book.js
|
||||
let reference_js_content = std::fs::read_to_string(
|
||||
temp_dir
|
||||
.path()
|
||||
.join("static-files-test-case-reference-635c9cdc.js"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!("book-e3b0c442.js", reference_js_content);
|
||||
// book.js winds up empty
|
||||
let book_js_content =
|
||||
std::fs::read_to_string(temp_dir.path().join("book-e3b0c442.js")).unwrap();
|
||||
assert_eq!("", book_js_content);
|
||||
}
|
||||
}
|
||||
@@ -111,11 +111,11 @@ function playground_text(playground, hidden = true) {
|
||||
let text = playground_text(code_block);
|
||||
let classes = code_block.querySelector('code').classList;
|
||||
let edition = "2015";
|
||||
if(classes.contains("edition2018")) {
|
||||
edition = "2018";
|
||||
} else if(classes.contains("edition2021")) {
|
||||
edition = "2021";
|
||||
}
|
||||
classes.forEach(className => {
|
||||
if (className.startsWith("edition")) {
|
||||
edition = className.slice(7);
|
||||
}
|
||||
});
|
||||
var params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
@@ -294,9 +294,9 @@ function playground_text(playground, hidden = true) {
|
||||
themeIds.push(el.id);
|
||||
});
|
||||
var stylesheets = {
|
||||
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
|
||||
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
|
||||
highlight: document.querySelector("[href$='highlight.css']"),
|
||||
ayuHighlight: document.querySelector("#ayu-highlight-css"),
|
||||
tomorrowNight: document.querySelector("#tomorrow-night-css"),
|
||||
highlight: document.querySelector("#highlight-css"),
|
||||
};
|
||||
|
||||
function showThemes() {
|
||||
@@ -449,6 +449,7 @@ function playground_text(playground, hidden = true) {
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||
var sidebarToggleAnchor = document.getElementById("sidebar-toggle-anchor");
|
||||
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
|
||||
var firstContact = null;
|
||||
|
||||
@@ -463,17 +464,6 @@ function playground_text(playground, hidden = true) {
|
||||
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
|
||||
}
|
||||
|
||||
|
||||
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
|
||||
|
||||
function toggleSection(ev) {
|
||||
ev.currentTarget.parentElement.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
Array.from(sidebarAnchorToggles).forEach(function (el) {
|
||||
el.addEventListener('click', toggleSection);
|
||||
});
|
||||
|
||||
function hideSidebar() {
|
||||
body.classList.remove('sidebar-visible')
|
||||
body.classList.add('sidebar-hidden');
|
||||
@@ -486,22 +476,16 @@ function playground_text(playground, hidden = true) {
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
|
||||
if (body.classList.contains("sidebar-hidden")) {
|
||||
sidebarToggleAnchor.addEventListener('change', function sidebarToggle() {
|
||||
if (sidebarToggleAnchor.checked) {
|
||||
var current_width = parseInt(
|
||||
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
|
||||
if (current_width < 150) {
|
||||
document.documentElement.style.setProperty('--sidebar-width', '150px');
|
||||
}
|
||||
showSidebar();
|
||||
} else if (body.classList.contains("sidebar-visible")) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (getComputedStyle(sidebar)['transform'] === 'none') {
|
||||
hideSidebar();
|
||||
} else {
|
||||
showSidebar();
|
||||
}
|
||||
hideSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -421,11 +421,14 @@ ul#searchresults span.teaser em {
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.sidebar-iframe-inner {
|
||||
--padding: 10px;
|
||||
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
padding: 10px 10px;
|
||||
padding: var(--padding);
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
color: var(--sidebar-fg);
|
||||
min-height: calc(100vh - var(--padding) * 2);
|
||||
}
|
||||
.sidebar-iframe-outer {
|
||||
border: none;
|
||||
|
||||
@@ -200,10 +200,12 @@ sup {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
:not(.footnote-definition) + .footnote-definition,
|
||||
.footnote-definition + :not(.footnote-definition) {
|
||||
:not(.footnote-definition) + .footnote-definition {
|
||||
margin-block-start: 2em;
|
||||
}
|
||||
.footnote-definition:not(:has(+ .footnote-definition)) {
|
||||
margin-block-end: 2em;
|
||||
}
|
||||
.footnote-definition {
|
||||
font-size: 0.9em;
|
||||
margin: 0.5em 0;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
--content-max-width: 750px;
|
||||
--menu-bar-height: 50px;
|
||||
--mono-font: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
--code-font-size: 0.875em /* please adjust the ace font size accordingly in editor.js */
|
||||
--code-font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'),
|
||||
url('open-sans-v17-all-charsets-300.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-300.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -16,7 +16,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
|
||||
url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-300italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -25,7 +25,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'),
|
||||
url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-regular.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -34,7 +34,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'),
|
||||
url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -43,7 +43,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
|
||||
url('open-sans-v17-all-charsets-600.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-600.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -52,7 +52,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-600italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -61,7 +61,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'),
|
||||
url('open-sans-v17-all-charsets-700.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-700.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -70,7 +70,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
|
||||
url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-700italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -79,7 +79,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
|
||||
url('open-sans-v17-all-charsets-800.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-800.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -88,7 +88,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-800italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -96,5 +96,5 @@
|
||||
font-family: 'Source Code Pro';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
|
||||
src: url('{{ resource "fonts/source-code-pro-v11-all-charsets-500.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
@@ -20,47 +20,49 @@
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
{{#if favicon_svg}}
|
||||
<link rel="icon" href="{{ path_to_root }}favicon.svg">
|
||||
<link rel="icon" href="{{ resource "favicon.svg" }}">
|
||||
{{/if}}
|
||||
{{#if favicon_png}}
|
||||
<link rel="shortcut icon" href="{{ path_to_root }}favicon.png">
|
||||
<link rel="shortcut icon" href="{{ resource "favicon.png" }}">
|
||||
{{/if}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
|
||||
{{/if}}
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="{{ resource "FontAwesome/css/font-awesome.css" }}">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
|
||||
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
|
||||
{{/if}}
|
||||
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}tomorrow-night.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}ayu-highlight.css">
|
||||
<link rel="stylesheet" id="highlight-css" href="{{ resource "highlight.css" }}">
|
||||
<link rel="stylesheet" id="tomorrow-night-css" href="{{ resource "tomorrow-night.css" }}">
|
||||
<link rel="stylesheet" id="ayu-highlight-css" href="{{ resource "ayu-highlight.css" }}">
|
||||
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
<link rel="stylesheet" href="{{ resource this }}">
|
||||
{{/each}}
|
||||
|
||||
{{#if mathjax_support}}
|
||||
<!-- MathJax -->
|
||||
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
</head>
|
||||
<body>
|
||||
<div id="body-container">
|
||||
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
var path_to_root = "{{ path_to_root }}";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
|
||||
</script>
|
||||
|
||||
<!-- Start loading toc.js asap -->
|
||||
<script src="{{ resource "toc.js" }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="body-container">
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script>
|
||||
try {
|
||||
@@ -107,7 +109,7 @@
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<!-- populated by js -->
|
||||
<div class="sidebar-scrollbox"></div>
|
||||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
||||
<noscript>
|
||||
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
|
||||
</noscript>
|
||||
@@ -116,8 +118,6 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script async src="{{ path_to_root }}toc.js"></script>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
@@ -280,26 +280,26 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_js}}
|
||||
<script src="{{ path_to_root }}ace.js"></script>
|
||||
<script src="{{ path_to_root }}editor.js"></script>
|
||||
<script src="{{ path_to_root }}mode-rust.js"></script>
|
||||
<script src="{{ path_to_root }}theme-dawn.js"></script>
|
||||
<script src="{{ path_to_root }}theme-tomorrow_night.js"></script>
|
||||
<script src="{{ resource "ace.js" }}"></script>
|
||||
<script src="{{ resource "editor.js" }}"></script>
|
||||
<script src="{{ resource "mode-rust.js" }}"></script>
|
||||
<script src="{{ resource "theme-dawn.js" }}"></script>
|
||||
<script src="{{ resource "theme-tomorrow_night.js" }}"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if search_js}}
|
||||
<script src="{{ path_to_root }}elasticlunr.min.js"></script>
|
||||
<script src="{{ path_to_root }}mark.min.js"></script>
|
||||
<script src="{{ path_to_root }}searcher.js"></script>
|
||||
<script src="{{ resource "elasticlunr.min.js" }}"></script>
|
||||
<script src="{{ resource "mark.min.js" }}"></script>
|
||||
<script src="{{ resource "searcher.js" }}"></script>
|
||||
{{/if}}
|
||||
|
||||
<script src="{{ path_to_root }}clipboard.min.js"></script>
|
||||
<script src="{{ path_to_root }}highlight.js"></script>
|
||||
<script src="{{ path_to_root }}book.js"></script>
|
||||
<script src="{{ resource "clipboard.min.js" }}"></script>
|
||||
<script src="{{ resource "highlight.js" }}"></script>
|
||||
<script src="{{ resource "book.js" }}"></script>
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script src="{{ ../path_to_root }}{{this}}"></script>
|
||||
<script src="{{ resource this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
|
||||
@@ -468,12 +468,12 @@ window.search = window.search || {};
|
||||
showResults(true);
|
||||
}
|
||||
|
||||
fetch(path_to_root + 'searchindex.json')
|
||||
fetch(path_to_root + '{{ resource "searchindex.json" }}')
|
||||
.then(response => response.json())
|
||||
.then(json => init(json))
|
||||
.catch(error => { // Try to load searchindex.js if fetch failed
|
||||
var script = document.createElement('script');
|
||||
script.src = path_to_root + 'searchindex.js';
|
||||
script.src = path_to_root + '{{ resource "searchindex.js" }}';
|
||||
script.onload = () => init(window.search);
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
@@ -21,20 +21,20 @@
|
||||
{{> head}}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
|
||||
{{/if}}
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="{{ resource "FontAwesome/css/font-awesome.css" }}">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
|
||||
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
|
||||
{{/if}}
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
<link rel="stylesheet" href="{{ resource this }}">
|
||||
{{/each}}
|
||||
</head>
|
||||
<body class="sidebar-iframe-inner">
|
||||
|
||||
@@ -3,52 +3,68 @@
|
||||
// This is a script, and not included directly in the page, to control the total size of the book.
|
||||
// The TOC contains an entry for each page, so if each page includes a copy of the TOC,
|
||||
// the total size of the page becomes O(n**2).
|
||||
var sidebarScrollbox = document.querySelector("#sidebar .sidebar-scrollbox");
|
||||
sidebarScrollbox.innerHTML = '{{#toc}}{{/toc}}';
|
||||
(function() {
|
||||
let current_page = document.location.href.toString();
|
||||
if (current_page.endsWith("/")) {
|
||||
current_page += "index.html";
|
||||
class MDBookSidebarScrollbox extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
var links = sidebarScrollbox.querySelectorAll("a");
|
||||
var l = links.length;
|
||||
for (var i = 0; i < l; ++i) {
|
||||
var link = links[i];
|
||||
var href = link.getAttribute("href");
|
||||
if (href && !href.startsWith("#") && !/^(?:[a-z+]+:)?\/\//.test(href)) {
|
||||
link.href = path_to_root + href;
|
||||
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];
|
||||
if (current_page.endsWith("/")) {
|
||||
current_page += "index.html";
|
||||
}
|
||||
// The "index" page is supposed to alias the first chapter in the book.
|
||||
if (link.href === current_page || (i === 0 && path_to_root === "" && current_page.endsWith("/index.html"))) {
|
||||
link.classList.add("active");
|
||||
var parent = link.parentElement;
|
||||
while (parent) {
|
||||
if (parent.tagName === "LI" && parent.previousElementSibling) {
|
||||
if (parent.previousElementSibling.classList.contains("chapter-item")) {
|
||||
parent.previousElementSibling.classList.add("expanded");
|
||||
}
|
||||
var links = Array.prototype.slice.call(this.querySelectorAll("a"));
|
||||
var l = links.length;
|
||||
for (var i = 0; i < l; ++i) {
|
||||
var link = links[i];
|
||||
var href = link.getAttribute("href");
|
||||
if (href && !href.startsWith("#") && !/^(?:[a-z+]+:)?\/\//.test(href)) {
|
||||
link.href = path_to_root + href;
|
||||
}
|
||||
// The "index" page is supposed to alias the first chapter in the book.
|
||||
if (link.href === current_page || (i === 0 && path_to_root === "" && current_page.endsWith("/index.html"))) {
|
||||
link.classList.add("active");
|
||||
var parent = link.parentElement;
|
||||
if (parent && parent.classList.contains("chapter-item")) {
|
||||
parent.classList.add("expanded");
|
||||
}
|
||||
while (parent) {
|
||||
if (parent.tagName === "LI" && parent.previousElementSibling) {
|
||||
if (parent.previousElementSibling.classList.contains("chapter-item")) {
|
||||
parent.previousElementSibling.classList.add("expanded");
|
||||
}
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Track and set sidebar scroll position
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
// Track and set sidebar scroll position
|
||||
this.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', this.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
this.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
// Toggle buttons
|
||||
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
|
||||
function toggleSection(ev) {
|
||||
ev.currentTarget.parentElement.classList.toggle('expanded');
|
||||
}
|
||||
Array.from(sidebarAnchorToggles).forEach(function (el) {
|
||||
el.addEventListener('click', toggleSection);
|
||||
});
|
||||
}
|
||||
}
|
||||
window.customElements.define("mdbook-sidebar-scrollbox", MDBookSidebarScrollbox);
|
||||
|
||||
@@ -9,6 +9,7 @@ edition = "2018"
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
hash-files = true
|
||||
|
||||
[output.html.playground]
|
||||
editable = true
|
||||
|
||||
@@ -23,7 +23,10 @@ fn failing_alternate_backend() {
|
||||
#[test]
|
||||
fn missing_backends_are_fatal() {
|
||||
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", false);
|
||||
assert!(md.build().is_err());
|
||||
let got = md.build();
|
||||
assert!(got.is_err());
|
||||
let error_message = got.err().unwrap().to_string();
|
||||
assert_eq!(error_message, "Rendering failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,3 +22,26 @@ fn base_mdbook_init_can_skip_confirmation_prompts() {
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
87
tests/gui/runner.rs
Normal file
87
tests/gui/runner.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::env::current_dir;
|
||||
use std::fs::{read_to_string, remove_dir_all};
|
||||
use std::process::Command;
|
||||
|
||||
fn get_available_browser_ui_test_version_inner(global: bool) -> Option<String> {
|
||||
let mut command = Command::new("npm");
|
||||
command
|
||||
.arg("list")
|
||||
.arg("--parseable")
|
||||
.arg("--long")
|
||||
.arg("--depth=0");
|
||||
if global {
|
||||
command.arg("--global");
|
||||
}
|
||||
let stdout = command.output().expect("`npm` command not found").stdout;
|
||||
let lines = String::from_utf8_lossy(&stdout);
|
||||
lines
|
||||
.lines()
|
||||
.find_map(|l| l.split(':').nth(1)?.strip_prefix("browser-ui-test@"))
|
||||
.map(std::borrow::ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn get_available_browser_ui_test_version() -> Option<String> {
|
||||
get_available_browser_ui_test_version_inner(false)
|
||||
.or_else(|| get_available_browser_ui_test_version_inner(true))
|
||||
}
|
||||
|
||||
fn expected_browser_ui_test_version() -> String {
|
||||
let content = read_to_string(".github/workflows/main.yml")
|
||||
.expect("failed to read `.github/workflows/main.yml`");
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
if let Some(version) = line.strip_prefix("BROWSER_UI_TEST_VERSION:") {
|
||||
return version.trim().replace('\'', "");
|
||||
}
|
||||
}
|
||||
panic!("failed to retrieved `browser-ui-test` version");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let browser_ui_test_version = expected_browser_ui_test_version();
|
||||
match get_available_browser_ui_test_version() {
|
||||
Some(version) => {
|
||||
if version != browser_ui_test_version {
|
||||
eprintln!(
|
||||
"⚠️ Installed version of browser-ui-test (`{version}`) is different than the \
|
||||
one used in the CI (`{browser_ui_test_version}`) You can install this version \
|
||||
using `npm update browser-ui-test` or by using `npm install browser-ui-test\
|
||||
@{browser_ui_test_version}`",
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
panic!(
|
||||
"`browser-ui-test` is not installed. You can install this package using `npm \
|
||||
update browser-ui-test` or by using `npm install browser-ui-test\
|
||||
@{browser_ui_test_version}`",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let current_dir = current_dir().expect("failed to retrieve current directory");
|
||||
let test_book = current_dir.join("test_book");
|
||||
|
||||
// Result doesn't matter.
|
||||
let _ = remove_dir_all(test_book.join("book"));
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.arg("run").arg("build").arg(&test_book);
|
||||
// Then we run the GUI tests on it.
|
||||
assert!(cmd.status().is_ok_and(|status| status.success()));
|
||||
|
||||
let book_dir = format!("file://{}", current_dir.join("test_book/book/").display());
|
||||
|
||||
let mut command = Command::new("npx");
|
||||
command
|
||||
.arg("browser-ui-test")
|
||||
.args(["--variable", "DOC_PATH", book_dir.as_str()])
|
||||
.args(["--test-folder", "tests/gui"]);
|
||||
if std::env::args().any(|arg| arg == "--disable-headless-test") {
|
||||
command.arg("--no-headless");
|
||||
}
|
||||
|
||||
// Then we run the GUI tests on it.
|
||||
let status = command.status().expect("failed to get command output");
|
||||
assert!(status.success(), "{status:?}");
|
||||
}
|
||||
33
tests/gui/search.goml
Normal file
33
tests/gui/search.goml
Normal file
@@ -0,0 +1,33 @@
|
||||
// This tests basic search behavior.
|
||||
|
||||
// We disable the requests checks because `searchindex.json` will always fail
|
||||
// locally (due to CORS), but the searchindex.js will succeed.
|
||||
fail-on-request-error: false
|
||||
go-to: |DOC_PATH| + "index.html"
|
||||
|
||||
define-function: (
|
||||
"open-search",
|
||||
[],
|
||||
block {
|
||||
assert-css: ("#search-wrapper", {"display": "none"})
|
||||
press-key: 'S'
|
||||
wait-for-css-false: ("#search-wrapper", {"display": "none"})
|
||||
}
|
||||
)
|
||||
|
||||
call-function: ("open-search", {})
|
||||
assert-text: ("#searchresults-header", "")
|
||||
write: "strikethrough"
|
||||
wait-for-text: ("#searchresults-header", "2 search results for 'strikethrough':")
|
||||
// Close the search display
|
||||
press-key: 'Escape'
|
||||
wait-for-css: ("#search-wrapper", {"display": "none"})
|
||||
// Reopening the search should show the last value
|
||||
call-function: ("open-search", {})
|
||||
assert-text: ("#searchresults-header", "2 search results for 'strikethrough':")
|
||||
// Navigate to a sub-chapter
|
||||
go-to: "./individual/strikethrough.html"
|
||||
assert-text: ("#searchresults-header", "")
|
||||
call-function: ("open-search", {})
|
||||
write: "strikethrough"
|
||||
wait-for-text: ("#searchresults-header", "2 search results for 'strikethrough':")
|
||||
16
tests/gui/sidebar-nojs.goml
Normal file
16
tests/gui/sidebar-nojs.goml
Normal file
@@ -0,0 +1,16 @@
|
||||
// This GUI test checks that the sidebar takes the whole height when it's inside
|
||||
// an iframe (because of JS disabled).
|
||||
// Regression test for <https://github.com/rust-lang/mdBook/issues/2528>.
|
||||
|
||||
// We disable the requests checks because `searchindex.json` will always fail
|
||||
// locally.
|
||||
fail-on-request-error: false
|
||||
// We disable javascript
|
||||
javascript: false
|
||||
go-to: |DOC_PATH| + "index.html"
|
||||
store-value: (height, 1000)
|
||||
set-window-size: (1000, |height|)
|
||||
|
||||
within-iframe: (".sidebar-iframe-outer", block {
|
||||
assert-size: (" body", {"height": |height|})
|
||||
})
|
||||
59
tests/gui/sidebar.goml
Normal file
59
tests/gui/sidebar.goml
Normal file
@@ -0,0 +1,59 @@
|
||||
// This GUI test checks sidebar hide/show and also its behaviour on smaller
|
||||
// width.
|
||||
|
||||
// We disable the requests checks because `searchindex.json` will always fail
|
||||
// locally.
|
||||
fail-on-request-error: false
|
||||
go-to: |DOC_PATH| + "index.html"
|
||||
set-window-size: (1100, 600)
|
||||
// Need to reload for the new size to be taken account by the JS.
|
||||
reload:
|
||||
|
||||
store-value: (content_indent, 308)
|
||||
|
||||
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})
|
||||
},
|
||||
)
|
||||
|
||||
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-position: ("#page-wrapper", {"x": 0})
|
||||
|
||||
// We expand the sidebar.
|
||||
click: "#sidebar-toggle"
|
||||
wait-for: "body.sidebar-visible"
|
||||
// `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|})
|
||||
},
|
||||
)
|
||||
|
||||
call-function: ("hide-sidebar", {})
|
||||
call-function: ("show-sidebar", {})
|
||||
|
||||
// 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", {})
|
||||
@@ -3,10 +3,11 @@ mod dummy_book;
|
||||
use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook};
|
||||
|
||||
use anyhow::Context;
|
||||
use mdbook::book::Chapter;
|
||||
use mdbook::config::Config;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils::fs::write_file;
|
||||
use mdbook::MDBook;
|
||||
use mdbook::{BookItem, MDBook};
|
||||
use pretty_assertions::assert_eq;
|
||||
use select::document::Document;
|
||||
use select::predicate::{Attr, Class, Name, Predicate};
|
||||
@@ -243,7 +244,7 @@ fn toc_js_html() -> Result<Document> {
|
||||
let toc_path = temp.path().join("book").join("toc.js");
|
||||
let html = fs::read_to_string(toc_path).with_context(|| "Unable to read index.html")?;
|
||||
for line in html.lines() {
|
||||
if let Some(left) = line.strip_prefix("sidebarScrollbox.innerHTML = '") {
|
||||
if let Some(left) = line.strip_prefix(" this.innerHTML = '") {
|
||||
if let Some(html) = left.strip_suffix("';") {
|
||||
return Ok(Document::from(html));
|
||||
}
|
||||
@@ -736,6 +737,7 @@ fn failure_on_missing_theme_directory() {
|
||||
#[cfg(feature = "search")]
|
||||
mod search {
|
||||
use crate::dummy_book::DummyBook;
|
||||
use mdbook::utils::fs::write_file;
|
||||
use mdbook::MDBook;
|
||||
use std::fs::{self, File};
|
||||
use std::path::Path;
|
||||
@@ -810,6 +812,51 @@ mod search {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_disable_individual_chapters() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let book_toml = r#"
|
||||
[book]
|
||||
title = "Search Test"
|
||||
|
||||
[output.html.search.chapter]
|
||||
"second" = { enable = false }
|
||||
"first/unicode.md" = { enable = false }
|
||||
"#;
|
||||
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
let index = read_book_index(temp.path());
|
||||
let doc_urls = index["doc_urls"].as_array().unwrap();
|
||||
let contains = |path| {
|
||||
doc_urls
|
||||
.iter()
|
||||
.any(|p| p.as_str().unwrap().starts_with(path))
|
||||
};
|
||||
assert!(contains("second.html"));
|
||||
assert!(!contains("second/"));
|
||||
assert!(!contains("first/unicode.html"));
|
||||
assert!(contains("first/markdown.html"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chapter_settings_validation_error() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let book_toml = r#"
|
||||
[book]
|
||||
title = "Search Test"
|
||||
|
||||
[output.html.search.chapter]
|
||||
"does-not-exist" = { enable = false }
|
||||
"#;
|
||||
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
let err = md.build().unwrap_err();
|
||||
assert!(format!("{err:?}").contains(
|
||||
"[output.html.search.chapter] key `does-not-exist` does not match any chapter paths"
|
||||
));
|
||||
}
|
||||
|
||||
// Setting this to `true` may cause issues with `cargo watch`,
|
||||
// since it may not finish writing the fixture before the tests
|
||||
// are run again.
|
||||
@@ -985,3 +1032,21 @@ fn custom_header_attributes() {
|
||||
];
|
||||
assert_contains_strings(&contents, summary_strings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_no_source_path() {
|
||||
// Test for a regression where search would fail if source_path is None.
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
let chapter = Chapter {
|
||||
name: "Sample chapter".to_string(),
|
||||
content: "".to_string(),
|
||||
number: None,
|
||||
sub_items: Vec::new(),
|
||||
path: Some(PathBuf::from("sample.html")),
|
||||
source_path: None,
|
||||
parent_names: Vec::new(),
|
||||
};
|
||||
md.book.sections.push(BookItem::Chapter(chapter));
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user