chore: update to 0.4.40 with absolute links via site-url
This commit is contained in:
parent
43815f35cc
commit
e1235853e3
45
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
labels: ["C-bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for filing a 🐛 bug report 😄!
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: >
|
||||
Please provide a clear and concise description of what the bug is,
|
||||
including what currently happens and what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps
|
||||
description: Please list the steps to reproduce the bug.
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
- type: textarea
|
||||
id: possible-solutions
|
||||
attributes:
|
||||
label: Possible Solution(s)
|
||||
description: >
|
||||
Not obligatory, but suggest a fix/reason for the bug,
|
||||
or ideas how to implement the addition or change.
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
description: Provide any additional notes that might be helpful.
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: >
|
||||
Please paste the output of running `mdbook --version` or which version
|
||||
of the library you are using.
|
||||
render: text
|
28
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
28
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: Enhancement
|
||||
description: Suggest an idea for enhancing mdBook
|
||||
labels: ["C-enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for filing a 🙋 feature request 😄!
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: >
|
||||
Please provide a clear description of your use case and the problem
|
||||
this feature request is trying to solve.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: >
|
||||
Please provide a clear and concise description of what you want to happen.
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
description: Provide any additional context or information that might be helpful.
|
24
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: Question
|
||||
description: Have a question on how to use mdBook?
|
||||
labels: ["C-question"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Got a question on how to do something with mdBook?
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question
|
||||
description: >
|
||||
Enter your question here. Please try to provide as much detail as possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: >
|
||||
Please paste the output of running `mdbook --version` or which version
|
||||
of the library you are using.
|
||||
render: text
|
67
.github/workflows/deploy.yml
vendored
Normal file
67
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
name: Deploy
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-20.04
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
name: Deploy ${{ matrix.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: ci/install-rust.sh stable ${{ matrix.target }}
|
||||
- name: Build asset
|
||||
run: ci/make-release-asset.sh ${{ matrix.os }} ${{ matrix.target }}
|
||||
- name: Update release with new asset
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh release upload $MDBOOK_TAG $MDBOOK_ASSET
|
||||
pages:
|
||||
name: GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Build book
|
||||
run: cargo run -- build guide
|
||||
- name: Deploy to GitHub
|
||||
env:
|
||||
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
|
||||
run: |
|
||||
touch guide/book/.nojekyll
|
||||
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
|
||||
cd guide/book
|
||||
/tmp/deploy
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo publish --no-verify
|
88
.github/workflows/main.yml
vendored
Normal file
88
.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: stable linux
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: beta linux
|
||||
os: ubuntu-latest
|
||||
rust: beta
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: nightly linux
|
||||
os: ubuntu-latest
|
||||
rust: nightly
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: stable x86_64-unknown-linux-musl
|
||||
os: ubuntu-20.04
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-musl
|
||||
- name: stable x86_64 macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
target: x86_64-apple-darwin
|
||||
- name: stable aarch64 macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
target: aarch64-apple-darwin
|
||||
- name: stable windows-msvc
|
||||
os: windows-latest
|
||||
rust: stable
|
||||
target: x86_64-pc-windows-msvc
|
||||
- name: msrv
|
||||
os: ubuntu-20.04
|
||||
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
|
||||
rust: 1.74.0
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: ${{ matrix.name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh ${{ matrix.rust }} ${{ matrix.target }}
|
||||
- name: Build and run tests
|
||||
run: cargo test --locked --target ${{ matrix.target }}
|
||||
- name: Test no default
|
||||
run: cargo test --no-default-features --target ${{ matrix.target }}
|
||||
|
||||
aarch64-cross-builds:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable aarch64-unknown-linux-musl
|
||||
- name: Build
|
||||
run: cargo build --locked --target aarch64-unknown-linux-musl
|
||||
|
||||
rustfmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Rust
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt --check
|
||||
|
||||
# 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
|
||||
# it easier to manage the list of jobs via this yml file and to prevent
|
||||
# accidentally adding new jobs without also updating the branch protections.
|
||||
success:
|
||||
name: Success gate
|
||||
if: always()
|
||||
needs:
|
||||
- test
|
||||
- rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'
|
||||
- name: Done
|
||||
run: exit 0
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
Cargo.lock
|
||||
target
|
||||
|
||||
# MacOS temp file
|
||||
|
71
CHANGELOG.md
71
CHANGELOG.md
@ -1,5 +1,76 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.4.40
|
||||
[v0.4.39...v0.4.40](https://github.com/rust-lang/mdBook/compare/v0.4.39...v0.4.40)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reverted the update to pulldown-cmark which broke the semver API.
|
||||
[#2388](https://github.com/rust-lang/mdBook/pull/2388)
|
||||
|
||||
## mdBook 0.4.39
|
||||
[v0.4.38...v0.4.39](https://github.com/rust-lang/mdBook/compare/v0.4.38...v0.4.39)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the automatic deploy broken in the previous release.
|
||||
[#2383](https://github.com/rust-lang/mdBook/pull/2383)
|
||||
|
||||
## mdBook 0.4.38
|
||||
[v0.4.37...v0.4.38](https://github.com/rust-lang/mdBook/compare/v0.4.37...v0.4.38)
|
||||
|
||||
### Added
|
||||
|
||||
- Added `nix` to the default set of languages supported for syntax highlighting.
|
||||
[#2262](https://github.com/rust-lang/mdBook/pull/2262)
|
||||
|
||||
### Changed
|
||||
|
||||
- The `output.html.curly-quotes` option has been renamed to `output.html.smart-punctuation` to better reflect what it does. The old option `curly-quotes` is kept for compatibility, but may be removed in the future.
|
||||
[#2327](https://github.com/rust-lang/mdBook/pull/2327)
|
||||
- The file-watcher used in `mdbook serve` and `mdbook watch` now uses a poll-based watcher instead of the native operating system notifications. This should fix issues on various systems and environments, and more accurately detect when files change. The native watcher can still be used with the `--watcher native` CLI option.
|
||||
[#2325](https://github.com/rust-lang/mdBook/pull/2325)
|
||||
- `mdbook test` output now includes color, and shows relative paths to the source.
|
||||
[#2259](https://github.com/rust-lang/mdBook/pull/2259)
|
||||
- Updated dependencies, MSRV raised to 1.74
|
||||
[#2350](https://github.com/rust-lang/mdBook/pull/2350)
|
||||
[#2351](https://github.com/rust-lang/mdBook/pull/2351)
|
||||
[#2378](https://github.com/rust-lang/mdBook/pull/2378)
|
||||
[#2381](https://github.com/rust-lang/mdBook/pull/2381)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reduced memory allocation when copying files.
|
||||
[#2355](https://github.com/rust-lang/mdBook/pull/2355)
|
||||
- Fixed the horizontal divider in `SUMMARY.md` from being indented into the previous nested section.
|
||||
[#2364](https://github.com/rust-lang/mdBook/pull/2364)
|
||||
- Removed unnecessary `@import` in the CSS.
|
||||
[#2260](https://github.com/rust-lang/mdBook/pull/2260)
|
||||
|
||||
## mdBook 0.4.37
|
||||
[v0.4.36...v0.4.37](https://github.com/rust-lang/mdBook/compare/v0.4.36...v0.4.37)
|
||||
|
||||
### Changed
|
||||
- ❗️ Updated the markdown parser. This brings in many changes to more closely follow the CommonMark spec. This may cause some small rendering changes. It is recommended to compare the output of the old and new version to check for changes. See <https://github.com/raphlinus/pulldown-cmark/releases/tag/v0.10.0> for more information.
|
||||
[#2308](https://github.com/rust-lang/mdBook/pull/2308)
|
||||
- The warning about the legacy `src/theme` directory has been removed.
|
||||
[#2263](https://github.com/rust-lang/mdBook/pull/2263)
|
||||
- Updated dependencies. MSRV raised to 1.71.0.
|
||||
[#2283](https://github.com/rust-lang/mdBook/pull/2283)
|
||||
[#2293](https://github.com/rust-lang/mdBook/pull/2293)
|
||||
[#2297](https://github.com/rust-lang/mdBook/pull/2297)
|
||||
[#2310](https://github.com/rust-lang/mdBook/pull/2310)
|
||||
[#2309](https://github.com/rust-lang/mdBook/pull/2309)
|
||||
- Some internal performance/memory improvements.
|
||||
[#2273](https://github.com/rust-lang/mdBook/pull/2273)
|
||||
[#2290](https://github.com/rust-lang/mdBook/pull/2290)
|
||||
- Made the `pathdiff` dependency optional based on the `watch` feature.
|
||||
[#2291](https://github.com/rust-lang/mdBook/pull/2291)
|
||||
|
||||
### Fixed
|
||||
- The `s` shortcut key handler should not trigger when focus is in an HTML form.
|
||||
[#2311](https://github.com/rust-lang/mdBook/pull/2311)
|
||||
|
||||
## mdBook 0.4.36
|
||||
[v0.4.35...v0.4.36](https://github.com/rust-lang/mdBook/compare/v0.4.35...v0.4.36)
|
||||
|
||||
|
@ -148,7 +148,7 @@ The following are instructions for updating [highlight.js](https://highlightjs.o
|
||||
1. Clone the repository at <https://github.com/highlightjs/highlight.js>
|
||||
1. Check out a tagged release (like `10.1.1`).
|
||||
1. Run `npm install`
|
||||
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx nim properties r scala x86asm yaml`
|
||||
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx nim nix properties r scala x86asm yaml`
|
||||
1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`.
|
||||
1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js).
|
||||
1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release.
|
||||
|
2309
Cargo.lock
generated
Normal file
2309
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.36"
|
||||
version = "0.4.40"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
@ -14,7 +14,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.70"
|
||||
rust-version = "1.74"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
@ -22,17 +22,16 @@ chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.3.12", features = ["cargo", "wrap_help"] }
|
||||
clap_complete = "4.3.2"
|
||||
once_cell = "1.17.1"
|
||||
env_logger = "0.10.0"
|
||||
handlebars = "5.0"
|
||||
env_logger = "0.11.1"
|
||||
handlebars = "6.0"
|
||||
log = "0.4.17"
|
||||
memchr = "2.5.0"
|
||||
opener = "0.6.1"
|
||||
pulldown-cmark = { version = "0.9.3", default-features = false }
|
||||
pathdiff = "0.2.1"
|
||||
opener = "0.7.0"
|
||||
pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] } # Do not update, part of the public api.
|
||||
regex = "1.8.1"
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
shlex = "1.1.0"
|
||||
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"
|
||||
@ -41,6 +40,8 @@ topological-sort = "0.2.2"
|
||||
notify = { version = "6.1.1", optional = true }
|
||||
notify-debouncer-mini = { version = "0.4.1", optional = true }
|
||||
ignore = { version = "0.4.20", optional = true }
|
||||
pathdiff = { version = "0.2.1", optional = true }
|
||||
walkdir = { version = "2.3.3", optional = true }
|
||||
|
||||
# Serve feature
|
||||
futures-util = { version = "0.3.28", optional = true }
|
||||
@ -49,7 +50,7 @@ warp = { version = "0.3.6", default-features = false, features = ["websocket"],
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "3.0.2", optional = true }
|
||||
ammonia = { version = "3.3.0", optional = true }
|
||||
ammonia = { version = "4.0.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.11"
|
||||
@ -61,7 +62,7 @@ walkdir = "2.3.3"
|
||||
|
||||
[features]
|
||||
default = ["watch", "serve", "search"]
|
||||
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore"]
|
||||
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"]
|
||||
serve = ["dep:futures-util", "dep:tokio", "dep:warp"]
|
||||
search = ["dep:elasticlunr-rs", "dep:ammonia"]
|
||||
|
||||
|
@ -22,6 +22,19 @@ then
|
||||
rustup component add llvm-tools-preview --toolchain=$TOOLCHAIN
|
||||
rustup component add rust-std-$TARGET --toolchain=$TOOLCHAIN
|
||||
fi
|
||||
if [[ $TARGET == *"musl" ]]
|
||||
then
|
||||
# This is needed by libdbus-sys.
|
||||
sudo apt update -y && sudo apt install musl-dev musl-tools -y
|
||||
fi
|
||||
if [[ $TARGET == "aarch64-unknown-linux-musl" ]]
|
||||
then
|
||||
echo CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=rust-lld >> $GITHUB_ENV
|
||||
# This `CC` is some nonsense needed for libdbus-sys (via opener).
|
||||
# I don't know if this is really the right thing to do, but it seems to work.
|
||||
sudo apt install gcc-aarch64-linux-gnu -y
|
||||
echo CC=aarch64-linux-gnu-gcc >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
|
||||
rustup default $TOOLCHAIN
|
||||
|
@ -12,10 +12,6 @@ TAG=${GITHUB_REF#*/tags/}
|
||||
|
||||
host=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
|
||||
target=$2
|
||||
if [ "$host" != "$target" ]
|
||||
then
|
||||
export "CARGO_TARGET_$(echo $target | tr a-z- A-Z_)_LINKER"=rust-lld
|
||||
fi
|
||||
export CARGO_PROFILE_RELEASE_LTO=true
|
||||
cargo build --locked --bin mdbook --release --target $target
|
||||
cd target/$target/release
|
||||
|
@ -26,7 +26,7 @@ fn main() {
|
||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||
handle_supports(&preprocessor, sub_args);
|
||||
} else if let Err(e) = handle_preprocessing(&preprocessor) {
|
||||
eprintln!("{}", e);
|
||||
eprintln!("{e}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ language = "en"
|
||||
edition = "2018"
|
||||
|
||||
[output.html]
|
||||
smart-punctuation = true
|
||||
mathjax-support = true
|
||||
site-url = "/mdBook/"
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
|
||||
|
@ -5,10 +5,10 @@ After you have [installed](../guide/installation.md) `mdbook`, you can run the `
|
||||
|
||||
This following sections provide in-depth information on the different commands available.
|
||||
|
||||
* [`mdbook init <directory>`](init.md) — Creates a new book with minimal boilerplate to start with.
|
||||
* [`mdbook build`](build.md) — Renders the book.
|
||||
* [`mdbook watch`](watch.md) — Rebuilds the book any time a source file changes.
|
||||
* [`mdbook serve`](serve.md) — Runs a web server to view the book, and rebuilds on changes.
|
||||
* [`mdbook test`](test.md) — Tests Rust code samples.
|
||||
* [`mdbook clean`](clean.md) — Deletes the rendered output.
|
||||
* [`mdbook completions`](completions.md) — Support for shell auto-completion.
|
||||
* [`mdbook init <directory>`](init.md) --- Creates a new book with minimal boilerplate to start with.
|
||||
* [`mdbook build`](build.md) --- Renders the book.
|
||||
* [`mdbook watch`](watch.md) --- Rebuilds the book any time a source file changes.
|
||||
* [`mdbook serve`](serve.md) --- Runs a web server to view the book, and rebuilds on changes.
|
||||
* [`mdbook test`](test.md) --- Tests Rust code samples.
|
||||
* [`mdbook clean`](clean.md) --- Deletes the rendered output.
|
||||
* [`mdbook completions`](completions.md) --- Support for shell auto-completion.
|
||||
|
7
guide/src/cli/arg-watcher.md
Normal file
7
guide/src/cli/arg-watcher.md
Normal file
@ -0,0 +1,7 @@
|
||||
#### `--watcher`
|
||||
|
||||
There are different backends used to determine when a file has changed.
|
||||
|
||||
* `poll` (default) --- Checks for file modifications by scanning the filesystem every second.
|
||||
* `native` --- Uses the native operating system facilities to receive notifications when files change.
|
||||
This can have less constant overhead, but may not be as reliable as the `poll` based watcher. See these issues for more information: [#383](https://github.com/rust-lang/mdBook/issues/383) [#1441](https://github.com/rust-lang/mdBook/issues/1441) [#1707](https://github.com/rust-lang/mdBook/issues/1707) [#2035](https://github.com/rust-lang/mdBook/issues/2035) [#2102](https://github.com/rust-lang/mdBook/issues/2102)
|
@ -7,8 +7,8 @@ mdbook build
|
||||
```
|
||||
|
||||
It will try to parse your `SUMMARY.md` file to understand the structure of your
|
||||
book and fetch the corresponding files. Note that files mentioned in `SUMMARY.md`
|
||||
but not present will be created.
|
||||
book and fetch the corresponding files. Note that this will also create files
|
||||
mentioned in `SUMMARY.md` which are not yet present.
|
||||
|
||||
The rendered output will maintain the same directory structure as the source for
|
||||
convenience. Large books will therefore remain structured when rendered.
|
||||
@ -22,12 +22,12 @@ root instead of the current working directory.
|
||||
mdbook build path/to/book
|
||||
```
|
||||
|
||||
#### --open
|
||||
#### `--open`
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the rendered book in
|
||||
your default web browser after building it.
|
||||
|
||||
#### --dest-dir
|
||||
#### `--dest-dir`
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
|
@ -16,7 +16,7 @@ root instead of the current working directory.
|
||||
mdbook clean path/to/book
|
||||
```
|
||||
|
||||
#### --dest-dir
|
||||
#### `--dest-dir`
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to override the book's output
|
||||
directory, which will be deleted by this command. Relative paths are interpreted
|
||||
@ -27,4 +27,4 @@ value of the `build.build-dir` key in `book.toml`, or to `./book`.
|
||||
mdbook clean --dest-dir=path/to/book
|
||||
```
|
||||
|
||||
`path/to/book` could be absolute or relative.
|
||||
`path/to/book` could be absolute or relative.
|
||||
|
@ -45,7 +45,7 @@ instead of the current working directory.
|
||||
mdbook init path/to/book
|
||||
```
|
||||
|
||||
#### --theme
|
||||
#### `--theme`
|
||||
|
||||
When you use the `--theme` flag, the default theme will be copied into a
|
||||
directory called `theme` in your source directory so that you can modify it.
|
||||
@ -53,7 +53,7 @@ directory called `theme` in your source directory so that you can modify it.
|
||||
The theme is selectively overwritten, this means that if you don't want to
|
||||
overwrite a specific file, just delete it and the default file will be used.
|
||||
|
||||
#### --title
|
||||
#### `--title`
|
||||
|
||||
Specify a title for the book. If not supplied, an interactive prompt will ask for
|
||||
a title.
|
||||
@ -62,7 +62,7 @@ a title.
|
||||
mdbook init --title="my amazing book"
|
||||
```
|
||||
|
||||
#### --ignore
|
||||
#### `--ignore`
|
||||
|
||||
Create a `.gitignore` file configured to ignore the `book` directory created when [building] a book.
|
||||
If not supplied, an interactive prompt will ask whether it should be created.
|
||||
@ -77,6 +77,6 @@ mdbook init --ignore=git
|
||||
|
||||
[building]: build.md
|
||||
|
||||
#### --force
|
||||
#### `--force`
|
||||
|
||||
Skip the prompts to create a `.gitignore` and for the title for the book.
|
||||
|
@ -32,18 +32,20 @@ The `serve` hostname defaults to `localhost`, and the port defaults to `3000`. E
|
||||
mdbook serve path/to/book -p 8000 -n 127.0.0.1
|
||||
```
|
||||
|
||||
#### --open
|
||||
#### `--open`
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in your
|
||||
default web browser after starting the server.
|
||||
|
||||
#### --dest-dir
|
||||
#### `--dest-dir`
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
{{#include arg-watcher.md}}
|
||||
|
||||
#### Specify exclude patterns
|
||||
|
||||
The `serve` command will not automatically trigger a build for files listed in
|
||||
|
@ -37,7 +37,7 @@ instead of the current working directory.
|
||||
mdbook test path/to/book
|
||||
```
|
||||
|
||||
#### --library-path
|
||||
#### `--library-path`
|
||||
|
||||
The `--library-path` (`-L`) option allows you to add directories to the library
|
||||
search path used by `rustdoc` when it builds and tests the examples. Multiple
|
||||
@ -54,14 +54,14 @@ mdbook test my-book -L target/debug/deps/
|
||||
See the `rustdoc` command-line [documentation](https://doc.rust-lang.org/rustdoc/command-line-arguments.html#-l--library-path-where-to-look-for-dependencies)
|
||||
for more information.
|
||||
|
||||
#### --dest-dir
|
||||
#### `--dest-dir`
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
#### --chapter
|
||||
#### `--chapter`
|
||||
|
||||
The `--chapter` (`-c`) option allows you to test a specific chapter of the
|
||||
book using the chapter name or the relative path to the chapter.
|
||||
|
@ -15,18 +15,19 @@ root instead of the current working directory.
|
||||
mdbook watch path/to/book
|
||||
```
|
||||
|
||||
#### --open
|
||||
#### `--open`
|
||||
|
||||
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
|
||||
your default web browser.
|
||||
|
||||
#### --dest-dir
|
||||
#### `--dest-dir`
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
{{#include arg-watcher.md}}
|
||||
|
||||
#### Specify exclude patterns
|
||||
|
||||
|
@ -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.36/mdbook-v0.4.36-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.40/mdbook-v0.4.40-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
bin/mdbook build
|
||||
```
|
||||
|
||||
@ -51,13 +51,13 @@ cargo install mdbook --no-default-features --features search --vers "^0.4" --loc
|
||||
|
||||
This includes several recommended options:
|
||||
|
||||
* `--no-default-features` — Disables features like the HTTP server used by `mdbook serve` that is likely not needed on CI.
|
||||
* `--no-default-features` --- Disables features like the HTTP server used by `mdbook serve` that is likely not needed on CI.
|
||||
This will speed up the build time significantly.
|
||||
* `--features search` — Disabling default features means you should then manually enable features that you want, such as the built-in [search] capability.
|
||||
* `--vers "^0.4"` — This will install the most recent version of the `0.4` series.
|
||||
* `--features search` --- Disabling default features means you should then manually enable features that you want, such as the built-in [search] capability.
|
||||
* `--vers "^0.4"` --- This will install the most recent version of the `0.4` series.
|
||||
However, versions after like `0.5.0` won't be installed, as they may break your build.
|
||||
Cargo will automatically upgrade mdBook if you have an older version already installed.
|
||||
* `--locked` — This will use the dependencies that were used when mdBook was released.
|
||||
* `--locked` --- This will use the dependencies that were used when mdBook was released.
|
||||
Without `--locked`, it will use the latest version of all dependencies, which may include some fixes since the last release, but may also (rarely) cause build problems.
|
||||
|
||||
You will likely want to investigate caching options, as building mdBook can be somewhat slow.
|
||||
|
@ -287,7 +287,7 @@ like this:
|
||||
+ if cfg.deny_odds && num_words % 2 == 1 {
|
||||
+ eprintln!("{} has an odd number of words!", ch.name);
|
||||
+ process::exit(1);
|
||||
}
|
||||
+ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2142
guide/src/for_developers/mdbook-wordcount/Cargo.lock
generated
Normal file
2142
guide/src/for_developers/mdbook-wordcount/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -72,7 +72,7 @@ edition = "2015" # the default edition for code blocks
|
||||
```
|
||||
|
||||
- **edition**: Rust edition to use by default for the code snippets. Default
|
||||
is "2015". Individual code blocks can be controlled with the `edition2015`,
|
||||
is `"2015"`. Individual code blocks can be controlled with the `edition2015`,
|
||||
`edition2018` or `edition2021` annotations, such as:
|
||||
|
||||
~~~text
|
||||
|
@ -4,9 +4,9 @@ Renderers (also called "backends") are responsible for creating the output of th
|
||||
|
||||
The following backends are built-in:
|
||||
|
||||
* [`html`](#html-renderer-options) — This renders the book to HTML.
|
||||
* [`html`](#html-renderer-options) --- This renders the book to HTML.
|
||||
This is enabled by default if no other `[output]` tables are defined in `book.toml`.
|
||||
* [`markdown`](#markdown-renderer) — This outputs the book as markdown after running the preprocessors.
|
||||
* [`markdown`](#markdown-renderer) --- This outputs the book as markdown after running the preprocessors.
|
||||
This is useful for debugging preprocessors.
|
||||
|
||||
The community has developed several backends.
|
||||
@ -97,7 +97,7 @@ description = "The example book covers examples."
|
||||
theme = "my-theme"
|
||||
default-theme = "light"
|
||||
preferred-dark-theme = "navy"
|
||||
curly-quotes = true
|
||||
smart-punctuation = true
|
||||
mathjax-support = false
|
||||
copy-fonts = true
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
@ -120,10 +120,12 @@ The following configuration options are available:
|
||||
'Change Theme' dropdown. Defaults to `light`.
|
||||
- **preferred-dark-theme:** The default dark theme. This theme will be used if
|
||||
the browser requests the dark version of the site via the
|
||||
['prefers-color-scheme'](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
|
||||
[`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
|
||||
CSS media query. Defaults to `navy`.
|
||||
- **curly-quotes:** Convert straight quotes to curly quotes, except for those
|
||||
that occur in code blocks and code spans. Defaults to `false`.
|
||||
- **smart-punctuation:** Converts quotes to curly quotes, `...` to `…`, `--` to en-dash, and `---` to em-dash.
|
||||
See [Smart Punctuation](../markdown.md#smart-punctuation).
|
||||
Defaults to `false`.
|
||||
- **curly-quotes:** Deprecated alias for `smart-punctuation`.
|
||||
- **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to
|
||||
`false`.
|
||||
- **copy-fonts:** (**Deprecated**) If `true` (the default), mdBook uses its built-in fonts which are copied to the output directory.
|
||||
|
@ -214,12 +214,12 @@ characters:
|
||||
So, no need to manually enter those Unicode characters!
|
||||
|
||||
This feature is disabled by default.
|
||||
To enable it, see the [`output.html.curly-quotes`] config option.
|
||||
To enable it, see the [`output.html.smart-punctuation`] config option.
|
||||
|
||||
[strikethrough]: https://github.github.com/gfm/#strikethrough-extension-
|
||||
[tables]: https://github.github.com/gfm/#tables-extension-
|
||||
[task list extension]: https://github.github.com/gfm/#task-list-items-extension-
|
||||
[`output.html.curly-quotes`]: configuration/renderers.md#html-renderer-options
|
||||
[`output.html.smart-punctuation`]: configuration/renderers.md#html-renderer-options
|
||||
|
||||
### Heading attributes
|
||||
|
||||
@ -232,4 +232,4 @@ Example:
|
||||
|
||||
This makes the level 1 heading with the content `Example heading`, ID `first`, and classes `class1` and `class2`. Note that the attributes should be space-separated.
|
||||
|
||||
More information can be found in the [heading attrs spec page](https://github.com/raphlinus/pulldown-cmark/blob/master/specs/heading_attrs.txt).
|
||||
More information can be found in the [heading attrs spec page](https://github.com/raphlinus/pulldown-cmark/blob/master/pulldown-cmark/specs/heading_attrs.txt).
|
||||
|
@ -112,16 +112,16 @@ panic!("oops!");
|
||||
These are particularly important when using [`mdbook test`] to test Rust examples.
|
||||
These use the same attributes as [rustdoc attributes], with a few additions:
|
||||
|
||||
* `editable` — Enables the [editor].
|
||||
* `noplayground` — Removes the play button, but will still be tested.
|
||||
* `mdbook-runnable` — Forces the play button to be displayed.
|
||||
* `editable` --- Enables the [editor].
|
||||
* `noplayground` --- Removes the play button, but will still be tested.
|
||||
* `mdbook-runnable` --- Forces the play button to be displayed.
|
||||
This is intended to be combined with the `ignore` attribute for examples that should not be tested, but you want to allow the reader to run.
|
||||
* `ignore` — Will not be tested and no play button is shown, but it is still highlighted as Rust syntax.
|
||||
* `should_panic` — When executed, it should produce a panic.
|
||||
* `no_run` — The code is compiled when tested, but it is not run.
|
||||
* `ignore` --- Will not be tested and no play button is shown, but it is still highlighted as Rust syntax.
|
||||
* `should_panic` --- When executed, it should produce a panic.
|
||||
* `no_run` --- The code is compiled when tested, but it is not run.
|
||||
The play button is also not shown.
|
||||
* `compile_fail` — The code should fail to compile.
|
||||
* `edition2015`, `edition2018`, `edition2021` — Forces the use of a specific Rust edition.
|
||||
* `compile_fail` --- The code should fail to compile.
|
||||
* `edition2015`, `edition2018`, `edition2021` --- Forces the use of a specific Rust edition.
|
||||
See [`rust.edition`] to set this globally.
|
||||
|
||||
[`mdbook test`]: ../cli/test.md
|
||||
|
@ -29,11 +29,12 @@ to be ignored at best, or may cause an error when attempting to build the book.
|
||||
- [First Chapter](relative/path/to/markdown2.md)
|
||||
```
|
||||
|
||||
1. ***Part Title*** - Headers can be used as a title for the following numbered
|
||||
chapters. This can be used to logically separate different sections
|
||||
of the book. The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many
|
||||
parts as desired.
|
||||
1. ***Part Title*** -
|
||||
Level 1 headers can be used as a title for the following numbered chapters.
|
||||
This can be used to logically separate different sections of the book.
|
||||
The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many parts as desired.
|
||||
Part titles must be h1 headers (one `#`), other heading levels are ignored.
|
||||
```markdown
|
||||
# My Part Title
|
||||
|
||||
|
@ -18,7 +18,7 @@ handlebars template you can access this information by using
|
||||
Here is a list of the properties that are exposed:
|
||||
|
||||
- ***language*** Language of the book in the form `en`, as specified in `book.toml` (if not specified, defaults to `en`). To use in <code
|
||||
class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||
class="language-html">\<html lang=\"{{ language }}\"></code> for example.
|
||||
- ***title*** Title used for the current page. This is identical to `{{ chapter_title }} - {{ book_title }}` unless `book_title` is not set in which case it just defaults to the `chapter_title`.
|
||||
- ***book_title*** Title of the book, as specified in `book.toml`
|
||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||
@ -79,7 +79,7 @@ var chapters = {{chapters}};
|
||||
|
||||
### 2. previous / next
|
||||
|
||||
The previous and next helpers expose a `link` and `name` property to the
|
||||
The previous and next helpers expose a `link` and `title` property to the
|
||||
previous and next chapters.
|
||||
|
||||
They are used like this
|
||||
@ -87,7 +87,7 @@ They are used like this
|
||||
```handlebars
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
<i class="fa fa-angle-left"></i> {{title}}
|
||||
</a>
|
||||
{{/previous}}
|
||||
```
|
||||
|
@ -45,6 +45,7 @@ your own `highlight.js` file:
|
||||
- markdown
|
||||
- nginx
|
||||
- nim
|
||||
- nix
|
||||
- objectivec
|
||||
- perl
|
||||
- php
|
||||
|
@ -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.70.
|
||||
mdBook currently requires at least Rust version 1.74.
|
||||
|
||||
Once you have installed Rust, the following command can be used to build and install mdBook:
|
||||
|
||||
@ -30,6 +30,9 @@ cargo install mdbook
|
||||
|
||||
This will automatically download mdBook from [crates.io], build it, and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||
|
||||
You can run `cargo install mdbook` again whenever you want to update to a new version.
|
||||
That command will check if there is a newer version, and re-install mdBook if a newer version is found.
|
||||
|
||||
To uninstall, run the command `cargo uninstall mdbook`.
|
||||
|
||||
[Rust installation page]: https://www.rust-lang.org/tools/install
|
||||
@ -47,6 +50,8 @@ cargo install --git https://github.com/rust-lang/mdBook.git mdbook
|
||||
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
## Modifying and contributing
|
||||
|
||||
If you are interested in making modifications to mdBook itself, check out the [Contributing Guide] for more information.
|
||||
|
||||
[Contributing Guide]: https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md
|
||||
|
@ -18,11 +18,11 @@ pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>
|
||||
|
||||
let mut summary_content = String::new();
|
||||
File::open(&summary_md)
|
||||
.with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))?
|
||||
.with_context(|| format!("Couldn't open SUMMARY.md in {src_dir:?} directory"))?
|
||||
.read_to_string(&mut summary_content)?;
|
||||
|
||||
let summary = parse_summary(&summary_content)
|
||||
.with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
|
||||
.with_context(|| format!("Summary parsing failed for file={summary_md:?}"))?;
|
||||
|
||||
if cfg.create_missing {
|
||||
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
|
||||
@ -160,8 +160,20 @@ pub struct Chapter {
|
||||
/// Nested items.
|
||||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
///
|
||||
/// **Note**: After the index preprocessor runs, any README files will be
|
||||
/// modified to be `index.md`. If you need access to the actual filename
|
||||
/// on disk, use [`Chapter::source_path`] instead.
|
||||
///
|
||||
/// This is `None` for a draft chapter.
|
||||
pub path: Option<PathBuf>,
|
||||
/// The chapter's source file, relative to the `SUMMARY.md` file.
|
||||
///
|
||||
/// **Note**: Beware that README files will internally be treated as
|
||||
/// `index.md` via the [`Chapter::path`] field. The `source_path` field
|
||||
/// exists if you need access to the true file path.
|
||||
///
|
||||
/// This is `None` for a draft chapter.
|
||||
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>,
|
||||
@ -329,7 +341,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
impl Display for Chapter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ref section_number) = self.number {
|
||||
write!(f, "{} ", section_number)?;
|
||||
write!(f, "{section_number} ")?;
|
||||
}
|
||||
|
||||
write!(f, "{}", self.name)
|
||||
@ -339,7 +351,6 @@ impl Display for Chapter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &str = "
|
||||
|
@ -15,10 +15,10 @@ pub use self::init::BookBuilder;
|
||||
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
|
||||
use log::{debug, error, info, log_enabled, trace, warn};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::ffi::OsString;
|
||||
use std::io::{IsTerminal, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::string::ToString;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
use topological_sort::TopologicalSort;
|
||||
@ -71,22 +71,28 @@ impl MDBook {
|
||||
|
||||
config.update_from_env();
|
||||
|
||||
if config
|
||||
.html_config()
|
||||
.map_or(false, |html| html.google_analytics.is_some())
|
||||
{
|
||||
warn!(
|
||||
"The output.html.google-analytics field has been deprecated; \
|
||||
it will be removed in a future release.\n\
|
||||
Consider placing the appropriate site tag code into the \
|
||||
theme/head.hbs file instead.\n\
|
||||
The tracking code may be found in the Google Analytics Admin page.\n\
|
||||
"
|
||||
);
|
||||
if let Some(html_config) = config.html_config() {
|
||||
if html_config.google_analytics.is_some() {
|
||||
warn!(
|
||||
"The output.html.google-analytics field has been deprecated; \
|
||||
it will be removed in a future release.\n\
|
||||
Consider placing the appropriate site tag code into the \
|
||||
theme/head.hbs file instead.\n\
|
||||
The tracking code may be found in the Google Analytics Admin page.\n\
|
||||
"
|
||||
);
|
||||
}
|
||||
if html_config.curly_quotes {
|
||||
warn!(
|
||||
"The output.html.curly-quotes field has been renamed to \
|
||||
output.html.smart-punctuation.\n\
|
||||
Use the new name in book.toml to remove this warning."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if log_enabled!(log::Level::Trace) {
|
||||
for line in format!("Config: {:#?}", config).lines() {
|
||||
for line in format!("Config: {config:#?}").lines() {
|
||||
trace!("{}", line);
|
||||
}
|
||||
}
|
||||
@ -259,10 +265,18 @@ impl MDBook {
|
||||
/// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
|
||||
/// If `chapter` is `None`, all tests will be run.
|
||||
pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
|
||||
let library_args: Vec<&str> = (0..library_paths.len())
|
||||
.map(|_| "-L")
|
||||
.zip(library_paths.into_iter())
|
||||
.flat_map(|x| vec![x.0, x.1])
|
||||
let cwd = std::env::current_dir()?;
|
||||
let library_args: Vec<OsString> = library_paths
|
||||
.into_iter()
|
||||
.flat_map(|path| {
|
||||
let path = Path::new(path);
|
||||
let path = if path.is_relative() {
|
||||
cwd.join(path).into_os_string()
|
||||
} else {
|
||||
path.to_path_buf().into_os_string()
|
||||
};
|
||||
[OsString::from("-L"), path]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
@ -289,6 +303,7 @@ impl MDBook {
|
||||
.collect();
|
||||
let (book, _) = self.preprocess_book(&TestRenderer)?;
|
||||
|
||||
let color_output = std::io::stderr().is_terminal();
|
||||
let mut failed = false;
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
@ -314,7 +329,10 @@ impl MDBook {
|
||||
tmpf.write_all(ch.content.as_bytes())?;
|
||||
|
||||
let mut cmd = Command::new("rustdoc");
|
||||
cmd.arg(&path).arg("--test").args(&library_args);
|
||||
cmd.current_dir(temp_dir.path())
|
||||
.arg(chapter_path)
|
||||
.arg("--test")
|
||||
.args(&library_args);
|
||||
|
||||
if let Some(edition) = self.config.rust.edition {
|
||||
match edition {
|
||||
@ -327,9 +345,16 @@ impl MDBook {
|
||||
RustEdition::E2021 => {
|
||||
cmd.args(["--edition", "2021"]);
|
||||
}
|
||||
RustEdition::E2024 => {
|
||||
cmd.args(["--edition", "2024", "-Zunstable-options"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if color_output {
|
||||
cmd.args(["--color", "always"]);
|
||||
}
|
||||
|
||||
debug!("running {:?}", cmd);
|
||||
let output = cmd.output()?;
|
||||
|
||||
@ -458,15 +483,13 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>
|
||||
if let Some(before) = table.get("before") {
|
||||
let before = before.as_array().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.before to be an array",
|
||||
name
|
||||
"Expected preprocessor.{name}.before to be an array"
|
||||
))
|
||||
})?;
|
||||
for after in before {
|
||||
let after = after.as_str().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.before to contain strings",
|
||||
name
|
||||
"Expected preprocessor.{name}.before to contain strings"
|
||||
))
|
||||
})?;
|
||||
|
||||
@ -485,16 +508,12 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>
|
||||
|
||||
if let Some(after) = table.get("after") {
|
||||
let after = after.as_array().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.after to be an array",
|
||||
name
|
||||
))
|
||||
Error::msg(format!("Expected preprocessor.{name}.after to be an array"))
|
||||
})?;
|
||||
for before in after {
|
||||
let before = before.as_str().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.after to contain strings",
|
||||
name
|
||||
"Expected preprocessor.{name}.after to contain strings"
|
||||
))
|
||||
})?;
|
||||
|
||||
@ -556,7 +575,7 @@ fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
|
||||
.get("command")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| format!("mdbook-{}", key))
|
||||
.unwrap_or_else(|| format!("mdbook-{key}"))
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
@ -567,7 +586,7 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
|
||||
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{key}"));
|
||||
|
||||
Box::new(CmdRenderer::new(key.to_string(), command))
|
||||
}
|
||||
@ -605,7 +624,7 @@ fn preprocessor_should_run(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use toml::value::{Table, Value};
|
||||
use toml::value::Table;
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_html_renderer_if_empty() {
|
||||
@ -761,7 +780,7 @@ mod tests {
|
||||
for preprocessor in &preprocessors {
|
||||
eprintln!(" {}", preprocessor.name());
|
||||
}
|
||||
panic!("{} should come before {}", before, after);
|
||||
panic!("{before} should come before {after}");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
use crate::errors::*;
|
||||
use log::{debug, trace, warn};
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Event, HeadingLevel, Tag};
|
||||
use memchr::Memchr;
|
||||
use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::iter::FromIterator;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@ -163,7 +162,7 @@ impl From<Link> for SummaryItem {
|
||||
/// > match the following regex: "[^<>\n[]]+".
|
||||
struct SummaryParser<'a> {
|
||||
src: &'a str,
|
||||
stream: pulldown_cmark::OffsetIter<'a, 'a>,
|
||||
stream: pulldown_cmark::OffsetIter<'a, DefaultBrokenLinkCallback>,
|
||||
offset: usize,
|
||||
|
||||
/// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
|
||||
@ -210,7 +209,7 @@ macro_rules! collect_events {
|
||||
}
|
||||
|
||||
impl<'a> SummaryParser<'a> {
|
||||
fn new(text: &str) -> SummaryParser<'_> {
|
||||
fn new(text: &'a str) -> SummaryParser<'a> {
|
||||
let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
|
||||
|
||||
SummaryParser {
|
||||
@ -265,7 +264,12 @@ impl<'a> SummaryParser<'a> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(ev @ Event::Start(Tag::List(..)))
|
||||
| Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
| Some(
|
||||
ev @ Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
}),
|
||||
) => {
|
||||
if is_prefix {
|
||||
// we've finished prefix chapters and are at the start
|
||||
// of the numbered section.
|
||||
@ -275,8 +279,8 @@ impl<'a> SummaryParser<'a> {
|
||||
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
|
||||
}
|
||||
}
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let link = self.parse_link(href.to_string());
|
||||
Some(Event::Start(Tag::Link { dest_url, .. })) => {
|
||||
let link = self.parse_link(dest_url.to_string());
|
||||
items.push(SummaryItem::Link(link));
|
||||
}
|
||||
Some(Event::Rule) => items.push(SummaryItem::Separator),
|
||||
@ -304,10 +308,13 @@ impl<'a> SummaryParser<'a> {
|
||||
break;
|
||||
}
|
||||
|
||||
Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
Some(Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
})) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
|
||||
let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
|
||||
Some(stringify_events(tags))
|
||||
}
|
||||
|
||||
@ -336,7 +343,7 @@ impl<'a> SummaryParser<'a> {
|
||||
/// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened.
|
||||
fn parse_link(&mut self, href: String) -> Link {
|
||||
let href = href.replace("%20", " ");
|
||||
let link_content = collect_events!(self.stream, end Tag::Link(..));
|
||||
let link_content = collect_events!(self.stream, end TagEnd::Link);
|
||||
let name = stringify_events(link_content);
|
||||
|
||||
let path = if href.is_empty() {
|
||||
@ -377,7 +384,12 @@ impl<'a> SummaryParser<'a> {
|
||||
}
|
||||
// The expectation is that pulldown cmark will terminate a paragraph before a new
|
||||
// heading, so we can always count on this to return without skipping headings.
|
||||
Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
Some(
|
||||
ev @ Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
}),
|
||||
) => {
|
||||
// we're starting a new part
|
||||
self.back(ev);
|
||||
break;
|
||||
@ -398,7 +410,7 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
// Skip over the contents of this tag
|
||||
while let Some(event) = self.next_event() {
|
||||
if event == Event::End(other_tag.clone()) {
|
||||
if event == Event::End(other_tag.clone().into()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -469,7 +481,7 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
last_item.nested_items = sub_items;
|
||||
}
|
||||
Some(Event::End(Tag::List(..))) => break,
|
||||
Some(Event::End(TagEnd::List(..))) => break,
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
@ -486,8 +498,8 @@ impl<'a> SummaryParser<'a> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Paragraph)) => continue,
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let mut link = self.parse_link(href.to_string());
|
||||
Some(Event::Start(Tag::Link { dest_url, .. })) => {
|
||||
let mut link = self.parse_link(dest_url.to_string());
|
||||
|
||||
let mut number = parent.clone();
|
||||
number.0.push(num_existing_items as u32 + 1);
|
||||
@ -529,14 +541,18 @@ impl<'a> SummaryParser<'a> {
|
||||
fn parse_title(&mut self) -> Option<String> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
Some(Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
})) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
|
||||
let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
|
||||
return Some(stringify_events(tags));
|
||||
}
|
||||
// Skip a HTML element such as a comment line.
|
||||
Some(Event::Html(_)) => {}
|
||||
Some(Event::Html(_) | Event::InlineHtml(_))
|
||||
| Some(Event::Start(Tag::HtmlBlock) | Event::End(TagEnd::HtmlBlock)) => {}
|
||||
// Otherwise, no title.
|
||||
Some(ev) => {
|
||||
self.back(ev);
|
||||
@ -567,11 +583,13 @@ fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
|
||||
.rev()
|
||||
.next()
|
||||
.ok_or_else(||
|
||||
anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links")
|
||||
.next_back()
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Unable to get last link because the list of SummaryItems \
|
||||
doesn't contain any Links"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes the styling from a list of Markdown events and returns just the
|
||||
@ -598,7 +616,7 @@ impl Display for SectionNumber {
|
||||
write!(f, "0")
|
||||
} else {
|
||||
for item in &self.0 {
|
||||
write!(f, "{}.", item)?;
|
||||
write!(f, "{item}.")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -744,8 +762,8 @@ mod tests {
|
||||
let _ = parser.stream.next(); // Discard opening paragraph
|
||||
|
||||
let href = match parser.stream.next() {
|
||||
Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(),
|
||||
other => panic!("Unreachable, {:?}", other),
|
||||
Some((Event::Start(Tag::Link { dest_url, .. }), _range)) => dest_url.to_string(),
|
||||
other => panic!("Unreachable, {other:?}"),
|
||||
};
|
||||
|
||||
let got = parser.parse_link(href);
|
||||
|
@ -36,6 +36,20 @@ pub trait CommandExt: Sized {
|
||||
fn arg_open(self) -> Self {
|
||||
self._arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "watch", feature = "serve"))]
|
||||
fn arg_watcher(self) -> Self {
|
||||
#[cfg(feature = "watch")]
|
||||
return self._arg(
|
||||
Arg::new("watcher")
|
||||
.long("watcher")
|
||||
.value_parser(["poll", "native"])
|
||||
.default_value("poll")
|
||||
.help("The filesystem watching technique"),
|
||||
);
|
||||
#[cfg(not(feature = "watch"))]
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandExt for Command {
|
||||
|
@ -6,7 +6,6 @@ use clap::builder::NonEmptyStringValueParser;
|
||||
use futures_util::sink::SinkExt;
|
||||
use futures_util::StreamExt;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils;
|
||||
use mdbook::utils::fs::get_404_output_file;
|
||||
use mdbook::MDBook;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
@ -43,18 +42,19 @@ pub fn make_subcommand() -> Command {
|
||||
.help("Port to use for HTTP connections"),
|
||||
)
|
||||
.arg_open()
|
||||
.arg_watcher()
|
||||
}
|
||||
|
||||
// Serve command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(book_dir)?;
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
let port = args.get_one::<String>("port").unwrap();
|
||||
let hostname = args.get_one::<String>("hostname").unwrap();
|
||||
let open_browser = args.get_flag("open");
|
||||
|
||||
let address = format!("{}:{}", hostname, port);
|
||||
let address = format!("{hostname}:{port}");
|
||||
|
||||
let update_config = |book: &mut MDBook| {
|
||||
book.config
|
||||
@ -89,7 +89,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
serve(build_dir, sockaddr, reload_tx, &file_404);
|
||||
});
|
||||
|
||||
let serving_url = format!("http://{}", address);
|
||||
let serving_url = format!("http://{address}");
|
||||
info!("Serving on: {}", serving_url);
|
||||
|
||||
if open_browser {
|
||||
@ -97,23 +97,12 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
watch::trigger_on_change(&book, move |paths, book_dir| {
|
||||
info!("Files changed: {:?}", paths);
|
||||
info!("Building book...");
|
||||
|
||||
// FIXME: This area is really ugly because we need to re-set livereload :(
|
||||
let result = MDBook::load(book_dir).and_then(|mut b| {
|
||||
update_config(&mut b);
|
||||
b.build()
|
||||
});
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to load the book");
|
||||
utils::log_backtrace(&e);
|
||||
} else {
|
||||
{
|
||||
let watcher = watch::WatcherKind::from_str(args.get_one::<String>("watcher").unwrap());
|
||||
watch::rebuild_on_change(watcher, &book_dir, &update_config, &move || {
|
||||
let _ = tx.send(Message::text("reload"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let _ = thread_handle.join();
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use super::command_prelude::*;
|
||||
use crate::get_book_dir;
|
||||
use clap::builder::NonEmptyStringValueParser;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use clap::ArgAction;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use std::path::PathBuf;
|
||||
|
211
src/cmd/watch.rs
211
src/cmd/watch.rs
@ -1,14 +1,11 @@
|
||||
use super::command_prelude::*;
|
||||
use crate::{get_book_dir, open};
|
||||
use ignore::gitignore::Gitignore;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use pathdiff::diff_paths;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
mod native;
|
||||
mod poller;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand() -> Command {
|
||||
@ -17,12 +14,28 @@ pub fn make_subcommand() -> Command {
|
||||
.arg_dest_dir()
|
||||
.arg_root_dir()
|
||||
.arg_open()
|
||||
.arg_watcher()
|
||||
}
|
||||
|
||||
pub enum WatcherKind {
|
||||
Poll,
|
||||
Native,
|
||||
}
|
||||
|
||||
impl WatcherKind {
|
||||
pub fn from_str(s: &str) -> WatcherKind {
|
||||
match s {
|
||||
"poll" => WatcherKind::Poll,
|
||||
"native" => WatcherKind::Native,
|
||||
_ => panic!("unsupported watcher {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(book_dir)?;
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
let update_config = |book: &mut MDBook| {
|
||||
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||
@ -41,42 +54,21 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
open(path);
|
||||
}
|
||||
|
||||
trigger_on_change(&book, |paths, book_dir| {
|
||||
info!("Files changed: {:?}\nBuilding book...\n", paths);
|
||||
let result = MDBook::load(book_dir).and_then(|mut b| {
|
||||
update_config(&mut b);
|
||||
b.build()
|
||||
});
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to build the book");
|
||||
utils::log_backtrace(&e);
|
||||
}
|
||||
});
|
||||
let watcher = WatcherKind::from_str(args.get_one::<String>("watcher").unwrap());
|
||||
rebuild_on_change(watcher, &book_dir, &update_config, &|| {});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
if paths.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
match find_gitignore(book_root) {
|
||||
Some(gitignore_path) => {
|
||||
let (ignore, err) = Gitignore::new(&gitignore_path);
|
||||
if let Some(err) = err {
|
||||
warn!(
|
||||
"error reading gitignore `{}`: {err}",
|
||||
gitignore_path.display()
|
||||
);
|
||||
}
|
||||
filter_ignored_files(ignore, paths)
|
||||
}
|
||||
None => {
|
||||
// There is no .gitignore file.
|
||||
paths.iter().map(|path| path.to_path_buf()).collect()
|
||||
}
|
||||
pub fn rebuild_on_change(
|
||||
kind: WatcherKind,
|
||||
book_dir: &Path,
|
||||
update_config: &dyn Fn(&mut MDBook),
|
||||
post_build: &dyn Fn(),
|
||||
) {
|
||||
match kind {
|
||||
WatcherKind::Poll => self::poller::rebuild_on_change(book_dir, update_config, post_build),
|
||||
WatcherKind::Native => self::native::rebuild_on_change(book_dir, update_config, post_build),
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,144 +78,3 @@ fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
|
||||
.map(|p| p.join(".gitignore"))
|
||||
.find(|p| p.exists())
|
||||
}
|
||||
|
||||
// Note: The usage of `canonicalize` may encounter occasional failures on the Windows platform, presenting a potential risk.
|
||||
// For more details, refer to [Pull Request #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981).
|
||||
fn filter_ignored_files(ignore: Gitignore, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
let ignore_root = ignore
|
||||
.path()
|
||||
.canonicalize()
|
||||
.expect("ignore root canonicalize error");
|
||||
|
||||
paths
|
||||
.iter()
|
||||
.filter(|path| {
|
||||
let relative_path =
|
||||
diff_paths(&path, &ignore_root).expect("One of the paths should be an absolute");
|
||||
!ignore
|
||||
.matched_path_or_any_parents(&relative_path, relative_path.is_dir())
|
||||
.is_ignore()
|
||||
})
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calls the closure when a book source file is changed, blocking indefinitely.
|
||||
pub fn trigger_on_change<F>(book: &MDBook, closure: F)
|
||||
where
|
||||
F: Fn(Vec<PathBuf>, &Path),
|
||||
{
|
||||
use notify::RecursiveMode::*;
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut debouncer = match notify_debouncer_mini::new_debouncer(Duration::from_secs(1), tx) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
std::process::exit(1)
|
||||
}
|
||||
};
|
||||
let watcher = debouncer.watcher();
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(&book.source_dir(), Recursive) {
|
||||
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let _ = watcher.watch(&book.theme_dir(), Recursive);
|
||||
|
||||
// Add the book.toml file to the watcher if it exists
|
||||
let _ = watcher.watch(&book.root.join("book.toml"), NonRecursive);
|
||||
|
||||
for dir in &book.config.build.extra_watch_dirs {
|
||||
let path = book.root.join(dir);
|
||||
let canonical_path = path.canonicalize().unwrap_or_else(|e| {
|
||||
error!("Error while watching extra directory {path:?}:\n {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
if let Err(e) = watcher.watch(&canonical_path, Recursive) {
|
||||
error!(
|
||||
"Error while watching extra directory {:?}:\n {:?}",
|
||||
canonical_path, e
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Listening for changes...");
|
||||
|
||||
loop {
|
||||
let first_event = rx.recv().unwrap();
|
||||
sleep(Duration::from_millis(50));
|
||||
let other_events = rx.try_iter();
|
||||
|
||||
let all_events = std::iter::once(first_event).chain(other_events);
|
||||
|
||||
let paths: Vec<_> = all_events
|
||||
.filter_map(|event| match event {
|
||||
Ok(events) => Some(events),
|
||||
Err(error) => {
|
||||
log::warn!("error while watching for changes: {error}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.map(|event| event.path)
|
||||
.collect();
|
||||
|
||||
// If we are watching files outside the current repository (via extra-watch-dirs), then they are definitionally
|
||||
// ignored by gitignore. So we handle this case by including such files into the watched paths list.
|
||||
let any_external_paths = paths.iter().filter(|p| !p.starts_with(&book.root)).cloned();
|
||||
let mut paths = remove_ignored_files(&book.root, &paths[..]);
|
||||
paths.extend(any_external_paths);
|
||||
|
||||
if !paths.is_empty() {
|
||||
closure(paths, &book.root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ignore::gitignore::GitignoreBuilder;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_filter_ignored_files() {
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
|
||||
let ignore = GitignoreBuilder::new(¤t_dir)
|
||||
.add_line(None, "*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
let should_remain = current_dir.join("record.text");
|
||||
let should_filter = current_dir.join("index.html");
|
||||
|
||||
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
|
||||
assert_eq!(remain, vec![should_remain])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_ignored_files_should_handle_parent_dir() {
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
|
||||
let ignore = GitignoreBuilder::new(¤t_dir)
|
||||
.add_line(None, "*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let parent_dir = current_dir.join("..");
|
||||
let should_remain = parent_dir.join("record.text");
|
||||
let should_filter = parent_dir.join("index.html");
|
||||
|
||||
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
|
||||
assert_eq!(remain, vec![should_remain])
|
||||
}
|
||||
}
|
||||
|
189
src/cmd/watch/native.rs
Normal file
189
src/cmd/watch/native.rs
Normal file
@ -0,0 +1,189 @@
|
||||
//! A filesystem watcher using native operating system facilities.
|
||||
|
||||
use ignore::gitignore::Gitignore;
|
||||
use mdbook::MDBook;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn rebuild_on_change(
|
||||
book_dir: &Path,
|
||||
update_config: &dyn Fn(&mut MDBook),
|
||||
post_build: &dyn Fn(),
|
||||
) {
|
||||
use notify::RecursiveMode::*;
|
||||
|
||||
let mut book = MDBook::load(book_dir).unwrap_or_else(|e| {
|
||||
error!("failed to load book: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut debouncer = match notify_debouncer_mini::new_debouncer(Duration::from_secs(1), tx) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
std::process::exit(1)
|
||||
}
|
||||
};
|
||||
let watcher = debouncer.watcher();
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(&book.source_dir(), Recursive) {
|
||||
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let _ = watcher.watch(&book.theme_dir(), Recursive);
|
||||
|
||||
// Add the book.toml file to the watcher if it exists
|
||||
let _ = watcher.watch(&book.root.join("book.toml"), NonRecursive);
|
||||
|
||||
for dir in &book.config.build.extra_watch_dirs {
|
||||
let path = book.root.join(dir);
|
||||
let canonical_path = path.canonicalize().unwrap_or_else(|e| {
|
||||
error!("Error while watching extra directory {path:?}:\n {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
if let Err(e) = watcher.watch(&canonical_path, Recursive) {
|
||||
error!(
|
||||
"Error while watching extra directory {:?}:\n {:?}",
|
||||
canonical_path, e
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Listening for changes...");
|
||||
|
||||
loop {
|
||||
let first_event = rx.recv().unwrap();
|
||||
sleep(Duration::from_millis(50));
|
||||
let other_events = rx.try_iter();
|
||||
|
||||
let all_events = std::iter::once(first_event).chain(other_events);
|
||||
|
||||
let paths: Vec<_> = all_events
|
||||
.filter_map(|event| match event {
|
||||
Ok(events) => Some(events),
|
||||
Err(error) => {
|
||||
log::warn!("error while watching for changes: {error}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.map(|event| event.path)
|
||||
.collect();
|
||||
|
||||
// If we are watching files outside the current repository (via extra-watch-dirs), then they are definitionally
|
||||
// ignored by gitignore. So we handle this case by including such files into the watched paths list.
|
||||
let any_external_paths = paths.iter().filter(|p| !p.starts_with(&book.root)).cloned();
|
||||
let mut paths = remove_ignored_files(&book.root, &paths[..]);
|
||||
paths.extend(any_external_paths);
|
||||
|
||||
if !paths.is_empty() {
|
||||
info!("Files changed: {paths:?}");
|
||||
match MDBook::load(book_dir) {
|
||||
Ok(mut b) => {
|
||||
update_config(&mut b);
|
||||
if let Err(e) = b.build() {
|
||||
error!("failed to build the book: {e:?}");
|
||||
} else {
|
||||
post_build();
|
||||
}
|
||||
book = b;
|
||||
}
|
||||
Err(e) => error!("failed to load book config: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
if paths.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
match super::find_gitignore(book_root) {
|
||||
Some(gitignore_path) => {
|
||||
let (ignore, err) = Gitignore::new(&gitignore_path);
|
||||
if let Some(err) = err {
|
||||
warn!(
|
||||
"error reading gitignore `{}`: {err}",
|
||||
gitignore_path.display()
|
||||
);
|
||||
}
|
||||
filter_ignored_files(ignore, paths)
|
||||
}
|
||||
None => {
|
||||
// There is no .gitignore file.
|
||||
paths.iter().map(|path| path.to_path_buf()).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The usage of `canonicalize` may encounter occasional failures on the Windows platform, presenting a potential risk.
|
||||
// For more details, refer to [Pull Request #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981).
|
||||
fn filter_ignored_files(ignore: Gitignore, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
let ignore_root = ignore
|
||||
.path()
|
||||
.canonicalize()
|
||||
.expect("ignore root canonicalize error");
|
||||
|
||||
paths
|
||||
.iter()
|
||||
.filter(|path| {
|
||||
let relative_path = pathdiff::diff_paths(&path, &ignore_root)
|
||||
.expect("One of the paths should be an absolute");
|
||||
!ignore
|
||||
.matched_path_or_any_parents(&relative_path, relative_path.is_dir())
|
||||
.is_ignore()
|
||||
})
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ignore::gitignore::GitignoreBuilder;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_filter_ignored_files() {
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
|
||||
let ignore = GitignoreBuilder::new(¤t_dir)
|
||||
.add_line(None, "*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
let should_remain = current_dir.join("record.text");
|
||||
let should_filter = current_dir.join("index.html");
|
||||
|
||||
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
|
||||
assert_eq!(remain, vec![should_remain])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_ignored_files_should_handle_parent_dir() {
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
|
||||
let ignore = GitignoreBuilder::new(¤t_dir)
|
||||
.add_line(None, "*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let parent_dir = current_dir.join("..");
|
||||
let should_remain = parent_dir.join("record.text");
|
||||
let should_filter = parent_dir.join("index.html");
|
||||
|
||||
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
|
||||
assert_eq!(remain, vec![should_remain])
|
||||
}
|
||||
}
|
386
src/cmd/watch/poller.rs
Normal file
386
src/cmd/watch/poller.rs
Normal file
@ -0,0 +1,386 @@
|
||||
//! A simple poll-based filesystem watcher.
|
||||
//!
|
||||
//! This exists because the native change notifications have historically had
|
||||
//! lots of problems. Various operating systems and different filesystems have
|
||||
//! had problems correctly reporting changes.
|
||||
|
||||
use ignore::gitignore::Gitignore;
|
||||
use mdbook::MDBook;
|
||||
use pathdiff::diff_paths;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::FileType;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// Calls the closure when a book source file is changed, blocking indefinitely.
|
||||
pub fn rebuild_on_change(
|
||||
book_dir: &Path,
|
||||
update_config: &dyn Fn(&mut MDBook),
|
||||
post_build: &dyn Fn(),
|
||||
) {
|
||||
let mut book = MDBook::load(book_dir).unwrap_or_else(|e| {
|
||||
error!("failed to load book: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let mut watcher = Watcher::new(book_dir);
|
||||
|
||||
info!("Watching for changes...");
|
||||
// Scan once to initialize the starting point.
|
||||
watcher.set_roots(&book);
|
||||
watcher.scan();
|
||||
|
||||
// Track average scan time, to help investigate if the poller is taking
|
||||
// undesirably long. This is not a rigorous benchmark, just a rough
|
||||
// estimate.
|
||||
const AVG_SIZE: usize = 60;
|
||||
let mut avgs = vec![0.0; AVG_SIZE];
|
||||
let mut avg_i = 0;
|
||||
|
||||
loop {
|
||||
std::thread::sleep(Duration::new(1, 0));
|
||||
watcher.set_roots(&book);
|
||||
let start = Instant::now();
|
||||
let paths = watcher.scan();
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
avgs[avg_i] = elapsed;
|
||||
avg_i += 1;
|
||||
if avg_i >= AVG_SIZE {
|
||||
avg_i = 0;
|
||||
let avg = avgs.iter().sum::<f64>() / (avgs.len() as f64);
|
||||
trace!(
|
||||
"scan average time: {avg:.2}s, scan size is {}",
|
||||
watcher.path_data.len()
|
||||
);
|
||||
}
|
||||
|
||||
if !paths.is_empty() {
|
||||
info!("Files changed: {paths:?}");
|
||||
match MDBook::load(book_dir) {
|
||||
Ok(mut b) => {
|
||||
update_config(&mut b);
|
||||
if let Err(e) = b.build() {
|
||||
error!("failed to build the book: {e:?}");
|
||||
} else {
|
||||
post_build();
|
||||
}
|
||||
book = b;
|
||||
}
|
||||
Err(e) => error!("failed to load book config: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct PathData {
|
||||
file_type: FileType,
|
||||
mtime: SystemTime,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
/// A very simple poll-watcher that scans for modified files.
|
||||
#[derive(Default)]
|
||||
struct Watcher {
|
||||
/// The root paths where it will recursively scan for changes.
|
||||
root_paths: Vec<PathBuf>,
|
||||
/// Data about files on disk.
|
||||
path_data: HashMap<PathBuf, PathData>,
|
||||
/// Filters paths that will be watched.
|
||||
ignore: Option<(PathBuf, Gitignore)>,
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
fn new(book_root: &Path) -> Watcher {
|
||||
// FIXME: ignore should be reloaded when it changes.
|
||||
let ignore = super::find_gitignore(book_root).map(|gitignore_path| {
|
||||
let (ignore, err) = Gitignore::new(&gitignore_path);
|
||||
if let Some(err) = err {
|
||||
warn!(
|
||||
"error reading gitignore `{}`: {err}",
|
||||
gitignore_path.display()
|
||||
);
|
||||
}
|
||||
// Note: The usage of `canonicalize` may encounter occasional
|
||||
// failures on the Windows platform, presenting a potential risk.
|
||||
// For more details, refer to [Pull Request
|
||||
// #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981).
|
||||
let ignore_path = ignore
|
||||
.path()
|
||||
.canonicalize()
|
||||
.expect("ignore root canonicalize error");
|
||||
(ignore_path, ignore)
|
||||
});
|
||||
|
||||
Watcher {
|
||||
ignore,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the root directories where scanning will start.
|
||||
fn set_roots(&mut self, book: &MDBook) {
|
||||
let mut root_paths = vec![
|
||||
book.source_dir(),
|
||||
book.theme_dir(),
|
||||
book.root.join("book.toml"),
|
||||
];
|
||||
root_paths.extend(
|
||||
book.config
|
||||
.build
|
||||
.extra_watch_dirs
|
||||
.iter()
|
||||
.map(|path| book.root.join(path)),
|
||||
);
|
||||
if let Some(html_config) = book.config.html_config() {
|
||||
root_paths.extend(
|
||||
html_config
|
||||
.additional_css
|
||||
.iter()
|
||||
.chain(html_config.additional_js.iter())
|
||||
.map(|path| book.root.join(path)),
|
||||
);
|
||||
}
|
||||
|
||||
self.root_paths = root_paths;
|
||||
}
|
||||
|
||||
/// Scans for changes.
|
||||
///
|
||||
/// Returns the paths that have changed.
|
||||
fn scan(&mut self) -> Vec<PathBuf> {
|
||||
let ignore = &self.ignore;
|
||||
let new_path_data: HashMap<_, _> = self
|
||||
.root_paths
|
||||
.iter()
|
||||
.filter(|root| root.exists())
|
||||
.flat_map(|root| {
|
||||
WalkDir::new(root)
|
||||
.follow_links(true)
|
||||
.into_iter()
|
||||
.filter_entry(|entry| {
|
||||
if let Some((ignore_path, ignore)) = ignore {
|
||||
let path = entry.path();
|
||||
// Canonicalization helps with removing `..` and
|
||||
// `.` entries, which can cause issues with
|
||||
// diff_paths.
|
||||
let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
let relative_path = diff_paths(&path, &ignore_path)
|
||||
.expect("One of the paths should be an absolute");
|
||||
if ignore
|
||||
.matched_path_or_any_parents(&relative_path, relative_path.is_dir())
|
||||
.is_ignore()
|
||||
{
|
||||
trace!("ignoring {path:?}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.filter_map(move |entry| {
|
||||
let entry = match entry {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
debug!("failed to scan {root:?}: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if entry.file_type().is_dir() {
|
||||
// Changes to directories themselves aren't
|
||||
// particularly interesting.
|
||||
return None;
|
||||
}
|
||||
let path = entry.path().to_path_buf();
|
||||
|
||||
let meta = match entry.metadata() {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
debug!("failed to scan {path:?}: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mtime = meta.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
let pd = PathData {
|
||||
file_type: meta.file_type(),
|
||||
mtime,
|
||||
size: meta.len(),
|
||||
};
|
||||
Some((path, pd))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut paths = Vec::new();
|
||||
for (new_path, new_data) in &new_path_data {
|
||||
match self.path_data.get(new_path) {
|
||||
Some(old_data) => {
|
||||
if new_data != old_data {
|
||||
paths.push(new_path.to_path_buf());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
paths.push(new_path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for old_path in self.path_data.keys() {
|
||||
if !new_path_data.contains_key(old_path) {
|
||||
paths.push(old_path.to_path_buf());
|
||||
}
|
||||
}
|
||||
self.path_data = new_path_data;
|
||||
paths
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Helper for testing the watcher.
|
||||
fn check_watch_behavior(
|
||||
gitignore_path: &str,
|
||||
gitignore: &str,
|
||||
book_root_path: &str,
|
||||
ignored: &[&str],
|
||||
not_ignored: &[&str],
|
||||
extra_setup: &dyn Fn(&Path),
|
||||
) {
|
||||
// Create the book and initialize things.
|
||||
let temp = tempfile::Builder::new()
|
||||
.prefix("mdbook-")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let root = temp.path();
|
||||
let book_root = root.join(book_root_path);
|
||||
// eprintln!("book_root={book_root:?}",);
|
||||
MDBook::init(&book_root).build().unwrap();
|
||||
std::fs::write(root.join(gitignore_path), gitignore).unwrap();
|
||||
let create = |paths: &[&str]| {
|
||||
let mut paths = paths
|
||||
.iter()
|
||||
.map(|path| root.join(path))
|
||||
.inspect(|path| {
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, "initial content").unwrap();
|
||||
})
|
||||
.map(|path| path.canonicalize().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
paths.sort();
|
||||
paths
|
||||
};
|
||||
let ignored = create(ignored);
|
||||
let not_ignored = create(not_ignored);
|
||||
extra_setup(&book_root);
|
||||
// Create a watcher and check its behavior.
|
||||
let book = MDBook::load(&book_root).unwrap();
|
||||
let mut watcher = Watcher::new(&book_root);
|
||||
watcher.set_roots(&book);
|
||||
// Do an initial scan to initialize its state.
|
||||
watcher.scan();
|
||||
// Verify the steady state is empty.
|
||||
let changed = watcher.scan();
|
||||
assert_eq!(changed, Vec::<PathBuf>::new());
|
||||
// Modify all files, and verify that only not_ignored are detected.
|
||||
for path in ignored.iter().chain(not_ignored.iter()) {
|
||||
std::fs::write(path, "modified").unwrap();
|
||||
}
|
||||
let changed = watcher.scan();
|
||||
let mut changed = changed
|
||||
.into_iter()
|
||||
.map(|p| p.canonicalize().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
changed.sort();
|
||||
assert_eq!(changed, not_ignored);
|
||||
// Verify again that steady state is empty.
|
||||
let changed = watcher.scan();
|
||||
assert_eq!(changed, Vec::<PathBuf>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignore() {
|
||||
// Basic gitignore test.
|
||||
check_watch_behavior(
|
||||
"foo/.gitignore",
|
||||
"*.tmp",
|
||||
"foo",
|
||||
&["foo/src/somefile.tmp"],
|
||||
&["foo/src/chapter.md"],
|
||||
&|_book_root| {},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignore_in_parent() {
|
||||
// gitignore is in the parent of the book
|
||||
check_watch_behavior(
|
||||
".gitignore",
|
||||
"*.tmp\nsomedir/\n/inroot\n/foo/src/inbook\n",
|
||||
"foo",
|
||||
&[
|
||||
"foo/src/somefile.tmp",
|
||||
"foo/src/somedir/somefile",
|
||||
"inroot/somefile",
|
||||
"foo/src/inbook/somefile",
|
||||
],
|
||||
&["foo/src/inroot/somefile"],
|
||||
&|_book_root| {},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignore_canonical() {
|
||||
// test with path with ..
|
||||
check_watch_behavior(
|
||||
".gitignore",
|
||||
"*.tmp\nsomedir/\n/foo/src/inbook\n",
|
||||
"bar/../foo",
|
||||
&[
|
||||
"foo/src/somefile.tmp",
|
||||
"foo/src/somedir/somefile",
|
||||
"foo/src/inbook/somefile",
|
||||
],
|
||||
&["foo/src/chapter.md"],
|
||||
&|_book_root| {},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scan_extra_watch() {
|
||||
// Check behavior with extra-watch-dirs
|
||||
check_watch_behavior(
|
||||
".gitignore",
|
||||
"*.tmp\n/outside-root/ignoreme\n/foo/examples/ignoreme\n",
|
||||
"foo",
|
||||
&[
|
||||
"foo/src/somefile.tmp",
|
||||
"foo/examples/example.tmp",
|
||||
"outside-root/somefile.tmp",
|
||||
"outside-root/ignoreme",
|
||||
"foo/examples/ignoreme",
|
||||
],
|
||||
&[
|
||||
"foo/src/chapter.md",
|
||||
"foo/examples/example.rs",
|
||||
"foo/examples/example2.rs",
|
||||
"outside-root/image.png",
|
||||
],
|
||||
&|book_root| {
|
||||
std::fs::write(
|
||||
book_root.join("book.toml"),
|
||||
r#"
|
||||
[book]
|
||||
title = "foo"
|
||||
|
||||
[build]
|
||||
extra-watch-dirs = [
|
||||
"examples",
|
||||
"../outside-root",
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@ use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use toml::value::Table;
|
||||
use toml::{self, Value};
|
||||
use toml::Value;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::utils::{self, toml_ext::TomlExt};
|
||||
@ -145,7 +145,7 @@ impl Config {
|
||||
if let serde_json::Value::Object(ref map) = parsed_value {
|
||||
// To `set` each `key`, we wrap them as `prefix.key`
|
||||
for (k, v) in map {
|
||||
let full_key = format!("{}.{}", key, k);
|
||||
let full_key = format!("{key}.{k}");
|
||||
self.set(&full_key, v).expect("unreachable");
|
||||
}
|
||||
return;
|
||||
@ -504,6 +504,9 @@ pub struct RustConfig {
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// Rust edition to use for the code.
|
||||
pub enum RustEdition {
|
||||
/// The 2024 edition of Rust
|
||||
#[serde(rename = "2024")]
|
||||
E2024,
|
||||
/// The 2021 edition of Rust
|
||||
#[serde(rename = "2021")]
|
||||
E2021,
|
||||
@ -526,7 +529,9 @@ pub struct HtmlConfig {
|
||||
/// The theme to use if the browser requests the dark version of the site.
|
||||
/// Defaults to 'navy'.
|
||||
pub preferred_dark_theme: Option<String>,
|
||||
/// Use "smart quotes" instead of the usual `"` character.
|
||||
/// Supports smart quotes, apostrophes, ellipsis, en-dash, and em-dash.
|
||||
pub smart_punctuation: bool,
|
||||
/// Deprecated alias for `smart_punctuation`.
|
||||
pub curly_quotes: bool,
|
||||
/// Should mathjax be enabled?
|
||||
pub mathjax_support: bool,
|
||||
@ -590,6 +595,7 @@ impl Default for HtmlConfig {
|
||||
theme: None,
|
||||
default_theme: None,
|
||||
preferred_dark_theme: None,
|
||||
smart_punctuation: false,
|
||||
curly_quotes: false,
|
||||
mathjax_support: false,
|
||||
copy_fonts: true,
|
||||
@ -623,6 +629,11 @@ impl HtmlConfig {
|
||||
None => root.join("theme"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if smart punctuation is enabled.
|
||||
pub fn smart_punctuation(&self) -> bool {
|
||||
self.smart_punctuation || self.curly_quotes
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for how to render the print icon, print.html, and print.css.
|
||||
@ -656,7 +667,7 @@ pub struct Fold {
|
||||
pub level: u8,
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playground.
|
||||
/// Configuration for tweaking how the HTML renderer handles the playground.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Playground {
|
||||
@ -685,22 +696,14 @@ impl Default for Playground {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles code blocks.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// Configuration for tweaking how the HTML renderer handles code blocks.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Code {
|
||||
/// A prefix string to hide lines per language (one or more chars).
|
||||
pub hidelines: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for Code {
|
||||
fn default() -> Code {
|
||||
Code {
|
||||
hidelines: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration of the search functionality of the HTML renderer.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
@ -798,7 +801,7 @@ mod tests {
|
||||
[output.html]
|
||||
theme = "./themedir"
|
||||
default-theme = "rust"
|
||||
curly-quotes = true
|
||||
smart-punctuation = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["./foo/bar/baz.css"]
|
||||
git-repository-url = "https://foo.com/"
|
||||
@ -845,7 +848,7 @@ mod tests {
|
||||
runnable: true,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
smart_punctuation: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
||||
theme: Some(PathBuf::from("./themedir")),
|
||||
@ -1025,7 +1028,7 @@ mod tests {
|
||||
[output.html]
|
||||
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
|
||||
theme = "my-theme"
|
||||
curly-quotes = true
|
||||
smart-punctuation = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
@ -1050,7 +1053,7 @@ mod tests {
|
||||
|
||||
let html_should_be = HtmlConfig {
|
||||
theme: Some(PathBuf::from("my-theme")),
|
||||
curly_quotes: true,
|
||||
smart_punctuation: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
|
||||
additional_js: vec![PathBuf::from("custom.js")],
|
||||
@ -1320,4 +1323,37 @@ mod tests {
|
||||
assert!(html_config.print.enable);
|
||||
assert!(!html_config.print.page_break);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curly_quotes_or_smart_punctuation() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
|
||||
[output.html]
|
||||
smart-punctuation = true
|
||||
"#;
|
||||
let config = Config::from_str(src).unwrap();
|
||||
assert_eq!(config.html_config().unwrap().smart_punctuation(), true);
|
||||
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
|
||||
[output.html]
|
||||
curly-quotes = true
|
||||
"#;
|
||||
let config = Config::from_str(src).unwrap();
|
||||
assert_eq!(config.html_config().unwrap().smart_punctuation(), true);
|
||||
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
"#;
|
||||
let config = Config::from_str(src).unwrap();
|
||||
assert_eq!(
|
||||
config.html_config().unwrap_or_default().smart_punctuation(),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -493,7 +493,7 @@ mod tests {
|
||||
let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@ -519,7 +519,7 @@ mod tests {
|
||||
let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@ -536,7 +536,7 @@ mod tests {
|
||||
fn test_find_links_with_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:20}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@ -555,7 +555,7 @@ mod tests {
|
||||
fn test_find_links_with_line_number() {
|
||||
let s = "Some random text with {{#include file.rs:10}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@ -574,7 +574,7 @@ mod tests {
|
||||
fn test_find_links_with_from_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@ -593,7 +593,7 @@ mod tests {
|
||||
fn test_find_links_with_to_range() {
|
||||
let s = "Some random text with {{#include file.rs::20}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@ -612,7 +612,7 @@ mod tests {
|
||||
fn test_find_links_with_full_range() {
|
||||
let s = "Some random text with {{#include file.rs::}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@ -631,7 +631,7 @@ mod tests {
|
||||
fn test_find_links_with_no_range_specified() {
|
||||
let s = "Some random text with {{#include file.rs}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@ -650,7 +650,7 @@ mod tests {
|
||||
fn test_find_links_with_anchor() {
|
||||
let s = "Some random text with {{#include file.rs:anchor}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@ -670,7 +670,7 @@ mod tests {
|
||||
let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@ -690,7 +690,7 @@ mod tests {
|
||||
more\n text {{#playground my.rs editable no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
@ -721,7 +721,7 @@ mod tests {
|
||||
no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(res.len(), 3);
|
||||
assert_eq!(
|
||||
res[0],
|
||||
|
@ -54,10 +54,13 @@ impl HtmlHandlebars {
|
||||
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
|
||||
}
|
||||
|
||||
let content = utils::render_markdown(&ch.content, ctx.html_config.curly_quotes);
|
||||
let content = utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation());
|
||||
|
||||
let fixed_content =
|
||||
utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
|
||||
let fixed_content = utils::render_markdown_with_path(
|
||||
&ch.content,
|
||||
ctx.html_config.smart_punctuation(),
|
||||
Some(path),
|
||||
);
|
||||
if !ctx.is_index && ctx.html_config.print.page_break {
|
||||
// Add page break between chapters
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
|
||||
@ -158,13 +161,13 @@ impl HtmlHandlebars {
|
||||
let content_404 = if let Some(ref filename) = html_config.input_404 {
|
||||
let path = src_dir.join(filename);
|
||||
std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("unable to open 404 input file {:?}", path))?
|
||||
.with_context(|| format!("unable to open 404 input file {path:?}"))?
|
||||
} else {
|
||||
// 404 input not explicitly configured try the default file 404.md
|
||||
let default_404_location = src_dir.join("404.md");
|
||||
if default_404_location.exists() {
|
||||
std::fs::read_to_string(&default_404_location).with_context(|| {
|
||||
format!("unable to open 404 input file {:?}", default_404_location)
|
||||
format!("unable to open 404 input file {default_404_location:?}")
|
||||
})?
|
||||
} else {
|
||||
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
|
||||
@ -172,7 +175,8 @@ impl HtmlHandlebars {
|
||||
.to_string()
|
||||
}
|
||||
};
|
||||
let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes);
|
||||
let html_content_404 =
|
||||
utils::render_markdown(&content_404, html_config.smart_punctuation());
|
||||
|
||||
let mut data_404 = data.clone();
|
||||
let base_url = if let Some(site_url) = &html_config.site_url {
|
||||
@ -210,7 +214,7 @@ impl HtmlHandlebars {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
|
||||
#[allow(clippy::let_and_return)]
|
||||
fn post_process(
|
||||
&self,
|
||||
rendered: String,
|
||||
@ -241,7 +245,7 @@ impl HtmlHandlebars {
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?;
|
||||
write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
|
||||
}
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
@ -838,11 +842,7 @@ fn insert_link_into_header(
|
||||
.unwrap_or_default();
|
||||
|
||||
format!(
|
||||
r##"<h{level} id="{id}"{classes}><a class="header" href="#{id}">{text}</a></h{level}>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content,
|
||||
classes = classes
|
||||
r##"<h{level} id="{id}"{classes}><a class="header" href="#{id}">{content}</a></h{level}>"##
|
||||
)
|
||||
}
|
||||
|
||||
@ -864,12 +864,7 @@ fn fix_code_blocks(html: &str) -> String {
|
||||
let classes = &caps[2].replace(',', " ");
|
||||
let after = &caps[3];
|
||||
|
||||
format!(
|
||||
r#"<code{before}class="{classes}"{after}>"#,
|
||||
before = before,
|
||||
classes = classes,
|
||||
after = after
|
||||
)
|
||||
format!(r#"<code{before}class="{classes}"{after}>"#)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
@ -906,6 +901,7 @@ fn add_playground_pre(
|
||||
Some(RustEdition::E2015) => " edition2015",
|
||||
Some(RustEdition::E2018) => " edition2018",
|
||||
Some(RustEdition::E2021) => " edition2021",
|
||||
Some(RustEdition::E2024) => " edition2024",
|
||||
None => "",
|
||||
}
|
||||
};
|
||||
@ -926,8 +922,7 @@ fn add_playground_pre(
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
|
||||
.into()
|
||||
format!("# #![allow(unused)]\n{attrs}#fn main() {{\n{code}#}}").into()
|
||||
};
|
||||
content
|
||||
}
|
||||
@ -943,8 +938,9 @@ fn add_playground_pre(
|
||||
/// Modifies all `<code>` blocks to convert "hidden" lines and to wrap them in
|
||||
/// a `<span class="boring">`.
|
||||
fn hide_lines(html: &str, code_config: &Code) -> String {
|
||||
let language_regex = Regex::new(r"\blanguage-(\w+)\b").unwrap();
|
||||
let hidelines_regex = Regex::new(r"\bhidelines=(\S+)").unwrap();
|
||||
static LANGUAGE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\blanguage-(\w+)\b").unwrap());
|
||||
static HIDELINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\bhidelines=(\S+)").unwrap());
|
||||
|
||||
CODE_BLOCK_RE
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let text = &caps[1];
|
||||
@ -959,12 +955,12 @@ fn hide_lines(html: &str, code_config: &Code) -> String {
|
||||
)
|
||||
} else {
|
||||
// First try to get the prefix from the code block
|
||||
let hidelines_capture = hidelines_regex.captures(classes);
|
||||
let hidelines_capture = HIDELINES_REGEX.captures(classes);
|
||||
let hidelines_prefix = match &hidelines_capture {
|
||||
Some(capture) => Some(&capture[1]),
|
||||
None => {
|
||||
// Then look up the prefix by language
|
||||
language_regex.captures(classes).and_then(|capture| {
|
||||
LANGUAGE_REGEX.captures(classes).and_then(|capture| {
|
||||
code_config.hidelines.get(&capture[1]).map(|p| p.as_str())
|
||||
})
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ fn find_chapter(
|
||||
}
|
||||
}
|
||||
|
||||
previous = Some(item.clone());
|
||||
previous = Some(item);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
|
@ -79,12 +79,6 @@ impl HelperDef for RenderToc {
|
||||
let mut is_first_chapter = ctx.data().get("is_index").is_some();
|
||||
|
||||
for item in chapters {
|
||||
// Spacer
|
||||
if item.get("spacer").is_some() {
|
||||
out.write("<li class=\"spacer\"></li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let (section, level) = if let Some(s) = item.get("section") {
|
||||
(s.as_str(), s.matches('.').count())
|
||||
} else {
|
||||
@ -119,10 +113,16 @@ impl HelperDef for RenderToc {
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
|
||||
write_li_open_tag(out, is_expanded, !item.contains_key("section"))?;
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
if item.contains_key("spacer") {
|
||||
out.write("<li class=\"spacer\"></li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Part title
|
||||
if let Some(title) = item.get("part") {
|
||||
out.write("<li class=\"part-title\">")?;
|
||||
@ -142,7 +142,6 @@ impl HelperDef for RenderToc {
|
||||
.unwrap()
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
.replace('\\', "/");
|
||||
|
||||
if base_url.is_empty() {
|
||||
// Add link
|
||||
out.write(&utils::fs::path_to_root(¤t_path))?;
|
||||
|
@ -50,7 +50,7 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
"searchindex.js",
|
||||
format!("Object.assign(window.search, {});", index).as_bytes(),
|
||||
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)?;
|
||||
@ -66,11 +66,24 @@ fn add_doc(
|
||||
index: &mut Index,
|
||||
doc_urls: &mut Vec<String>,
|
||||
anchor_base: &str,
|
||||
section_id: &Option<String>,
|
||||
heading: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
section_id: &Option<CowStr<'_>>,
|
||||
items: &[&str],
|
||||
) {
|
||||
let url = if let Some(ref id) = *section_id {
|
||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||
// Either use the explicit section id the user specified, or generate one
|
||||
// from the heading content.
|
||||
let section_id = section_id.as_ref().map(|id| id.to_string()).or_else(|| {
|
||||
if heading.is_empty() {
|
||||
// In the case where a chapter has no heading, don't set a section id.
|
||||
None
|
||||
} else {
|
||||
Some(utils::unique_id_from_content(heading, id_counter))
|
||||
}
|
||||
});
|
||||
|
||||
let url = if let Some(id) = section_id {
|
||||
Cow::Owned(format!("{anchor_base}#{id}"))
|
||||
} else {
|
||||
Cow::Borrowed(anchor_base)
|
||||
};
|
||||
@ -119,7 +132,7 @@ fn render_item(
|
||||
let mut id_counter = HashMap::new();
|
||||
while let Some(event) = p.next() {
|
||||
match event {
|
||||
Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
|
||||
Event::Start(Tag::Heading { level, id, .. }) if level as u32 <= max_section_depth => {
|
||||
if !heading.is_empty() {
|
||||
// Section finished, the next heading is following now
|
||||
// Write the data to the index, and clear it for the next section
|
||||
@ -127,22 +140,21 @@ fn render_item(
|
||||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
&heading,
|
||||
&mut id_counter,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
section_id = None;
|
||||
heading.clear();
|
||||
body.clear();
|
||||
breadcrumbs.pop();
|
||||
}
|
||||
|
||||
section_id = id;
|
||||
in_heading = true;
|
||||
}
|
||||
Event::End(Tag::Heading(i, id, _classes)) if i as u32 <= max_section_depth => {
|
||||
Event::End(TagEnd::Heading(level)) if level as u32 <= max_section_depth => {
|
||||
in_heading = false;
|
||||
section_id = id
|
||||
.map(|id| id.to_string())
|
||||
.or_else(|| Some(utils::unique_id_from_content(&heading, &mut id_counter)));
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||
@ -159,9 +171,19 @@ fn render_item(
|
||||
html_block.push_str(html);
|
||||
p.next();
|
||||
}
|
||||
|
||||
body.push_str(&clean_html(&html_block));
|
||||
}
|
||||
Event::InlineHtml(html) => {
|
||||
// This is not capable of cleaning inline tags like
|
||||
// `foo <script>…</script>`. The `<script>` tags show up as
|
||||
// individual InlineHtml events, and the content inside is
|
||||
// just a regular Text event. There isn't a very good way to
|
||||
// know how to collect all the content in-between. I'm not
|
||||
// sure if this is easily fixable. It should be extremely
|
||||
// rare, since script and style tags should almost always be
|
||||
// blocks, and worse case you have some noise in the index.
|
||||
body.push_str(&clean_html(&html));
|
||||
}
|
||||
Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => {
|
||||
// Insert spaces where HTML output would usually separate text
|
||||
// to ensure words don't get merged together
|
||||
@ -181,25 +203,31 @@ fn render_item(
|
||||
Event::FootnoteReference(name) => {
|
||||
let len = footnote_numbers.len() + 1;
|
||||
let number = footnote_numbers.entry(name).or_insert(len);
|
||||
body.push_str(&format!(" [{}] ", number));
|
||||
body.push_str(&format!(" [{number}] "));
|
||||
}
|
||||
Event::TaskListMarker(_checked) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !body.is_empty() || !heading.is_empty() {
|
||||
if heading.is_empty() {
|
||||
let title = if heading.is_empty() {
|
||||
if let Some(chapter) = breadcrumbs.first() {
|
||||
heading = chapter.clone();
|
||||
chapter
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
&heading
|
||||
};
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
&heading,
|
||||
&mut id_counter,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
&[title, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -225,7 +225,7 @@ function playground_text(playground, hidden = true) {
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
clipButton.className = 'fa fa-copy clip-button';
|
||||
clipButton.className = 'clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
@ -258,7 +258,7 @@ function playground_text(playground, hidden = true) {
|
||||
|
||||
if (window.playground_copyable) {
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||
copyCodeClipboardButton.className = 'clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
@ -597,12 +597,12 @@ function playground_text(playground, hidden = true) {
|
||||
|
||||
function hideTooltip(elem) {
|
||||
elem.firstChild.innerText = "";
|
||||
elem.className = 'fa fa-copy clip-button';
|
||||
elem.className = 'clip-button';
|
||||
}
|
||||
|
||||
function showTooltip(elem, msg) {
|
||||
elem.firstChild.innerText = msg;
|
||||
elem.className = 'fa fa-copy tooltipped';
|
||||
elem.className = 'clip-button tooltipped';
|
||||
}
|
||||
|
||||
var clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||
|
@ -1,7 +1,5 @@
|
||||
/* CSS for UI elements (a.k.a. chrome) */
|
||||
|
||||
@import 'variables.css';
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--scrollbar) var(--bg);
|
||||
}
|
||||
@ -252,8 +250,8 @@ pre > .buttons i {
|
||||
pre > .buttons button {
|
||||
cursor: inherit;
|
||||
margin: 0px 5px;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
padding: 4px 4px 3px 5px;
|
||||
font-size: 23px;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
@ -264,6 +262,27 @@ pre > .buttons button {
|
||||
transition-property: color,border-color,background-color;
|
||||
color: var(--icons);
|
||||
}
|
||||
|
||||
pre > .buttons button.clip-button {
|
||||
padding: 2px 4px 0px 6px;
|
||||
}
|
||||
pre > .buttons button.clip-button::before {
|
||||
/* clipboard image from octicons (https://github.com/primer/octicons/tree/v2.0.0) MIT license
|
||||
*/
|
||||
content: url('data:image/svg+xml,<svg width="21" height="20" viewBox="0 0 24 25" \
|
||||
xmlns="http://www.w3.org/2000/svg" aria-label="Copy to clipboard">\
|
||||
<path d="M18 20h2v3c0 1-1 2-2 2H2c-.998 0-2-1-2-2V5c0-.911.755-1.667 1.667-1.667h5A3.323 3.323 0 \
|
||||
0110 0a3.323 3.323 0 013.333 3.333h5C19.245 3.333 20 4.09 20 5v8.333h-2V9H2v14h16v-3zM3 \
|
||||
7h14c0-.911-.793-1.667-1.75-1.667H13.5c-.957 0-1.75-.755-1.75-1.666C11.75 2.755 10.957 2 10 \
|
||||
2s-1.75.755-1.75 1.667c0 .911-.793 1.666-1.75 1.666H4.75C3.793 5.333 3 6.09 3 7z"/>\
|
||||
<path d="M4 19h6v2H4zM12 11H4v2h8zM4 17h4v-2H4zM15 15v-3l-4.5 4.5L15 21v-3l8.027-.032L23 15z"/>\
|
||||
</svg>');
|
||||
filter: var(--copy-button-filter);
|
||||
}
|
||||
pre > .buttons button.clip-button:hover::before {
|
||||
filter: var(--copy-button-filter-hover);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
pre > .buttons button {
|
||||
/* On mobile, make it easier to tap buttons. */
|
||||
|
@ -1,7 +1,5 @@
|
||||
/* Base styles and content styles */
|
||||
|
||||
@import 'variables.css';
|
||||
|
||||
:root {
|
||||
/* Browser default font-size is 16px, this way 1 rem = 10px */
|
||||
font-size: 62.5%;
|
||||
|
@ -56,6 +56,11 @@
|
||||
--search-mark-bg: #e3b171;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(45%) sepia(6%) saturate(621%) hue-rotate(198deg) brightness(99%) contrast(85%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(68%) sepia(55%) saturate(531%) hue-rotate(341deg) brightness(104%) contrast(101%);
|
||||
}
|
||||
|
||||
.coal {
|
||||
@ -100,6 +105,11 @@
|
||||
--search-mark-bg: #355c7d;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%);
|
||||
}
|
||||
|
||||
.light {
|
||||
@ -144,6 +154,11 @@
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: light;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(45.49%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(14%) sepia(93%) saturate(4250%) hue-rotate(243deg) brightness(99%) contrast(130%);
|
||||
}
|
||||
|
||||
.navy {
|
||||
@ -188,6 +203,11 @@
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(46%) sepia(20%) saturate(1537%) hue-rotate(156deg) brightness(85%) contrast(90%);
|
||||
}
|
||||
|
||||
.rust {
|
||||
@ -231,7 +251,10 @@
|
||||
--searchresults-li-bg: #dec2a2;
|
||||
--search-mark-bg: #e69f67;
|
||||
|
||||
--color-scheme: light;
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(77%) sepia(16%) saturate(1798%) hue-rotate(328deg) brightness(98%) contrast(83%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@ -275,5 +298,10 @@
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -212,7 +212,6 @@ fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
|
||||
#[test]
|
||||
|
@ -316,7 +316,7 @@ window.search = window.search || {};
|
||||
|
||||
// Eventhandler for keyevents on `document`
|
||||
function globalKeyHandler(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; }
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)) { return; }
|
||||
|
||||
if (e.keyCode === ESCAPE_KEYCODE) {
|
||||
e.preventDefault();
|
||||
|
@ -1,6 +1,5 @@
|
||||
use crate::errors::*;
|
||||
use log::{debug, trace};
|
||||
use std::convert::Into;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
@ -73,14 +72,12 @@ pub fn create_file(path: &Path) -> Result<File> {
|
||||
|
||||
/// Removes all the content of a directory but not the directory itself
|
||||
pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||
for item in fs::read_dir(dir)? {
|
||||
if let Ok(item) = item {
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
fs::remove_dir_all(item)?;
|
||||
} else {
|
||||
fs::remove_file(item)?;
|
||||
}
|
||||
for item in fs::read_dir(dir)?.flatten() {
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
fs::remove_dir_all(item)?;
|
||||
} else {
|
||||
fs::remove_file(item)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@ -109,72 +106,41 @@ pub fn copy_files_except_ext(
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(from)? {
|
||||
let entry = entry?;
|
||||
let entry = entry?.path();
|
||||
let metadata = entry
|
||||
.path()
|
||||
.metadata()
|
||||
.with_context(|| format!("Failed to read {:?}", entry.path()))?;
|
||||
.with_context(|| format!("Failed to read {entry:?}"))?;
|
||||
|
||||
let entry_file_name = entry.file_name().unwrap();
|
||||
let target_file_path = to.join(entry_file_name);
|
||||
|
||||
// If the entry is a dir and the recursive option is enabled, call itself
|
||||
if metadata.is_dir() && recursive {
|
||||
if entry.path() == to.to_path_buf() {
|
||||
if entry == to.as_os_str() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(avoid) = avoid_dir {
|
||||
if entry.path() == *avoid {
|
||||
if entry == *avoid {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// check if output dir already exists
|
||||
if !to.join(entry.file_name()).exists() {
|
||||
fs::create_dir(&to.join(entry.file_name()))?;
|
||||
if !target_file_path.exists() {
|
||||
fs::create_dir(&target_file_path)?;
|
||||
}
|
||||
|
||||
copy_files_except_ext(
|
||||
&from.join(entry.file_name()),
|
||||
&to.join(entry.file_name()),
|
||||
true,
|
||||
avoid_dir,
|
||||
ext_blacklist,
|
||||
)?;
|
||||
copy_files_except_ext(&entry, &target_file_path, true, avoid_dir, ext_blacklist)?;
|
||||
} else if metadata.is_file() {
|
||||
// Check if it is in the blacklist
|
||||
if let Some(ext) = entry.path().extension() {
|
||||
if let Some(ext) = entry.extension() {
|
||||
if ext_blacklist.contains(&ext.to_str().unwrap()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
"creating path for file: {:?}",
|
||||
&to.join(
|
||||
entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")
|
||||
)
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Copying {:?} to {:?}",
|
||||
entry.path(),
|
||||
&to.join(
|
||||
entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")
|
||||
)
|
||||
);
|
||||
copy(
|
||||
entry.path(),
|
||||
&to.join(
|
||||
entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name..."),
|
||||
),
|
||||
)?;
|
||||
debug!("Copying {entry:?} to {target_file_path:?}");
|
||||
copy(&entry, &target_file_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@ -262,47 +228,47 @@ mod tests {
|
||||
fn copy_files_except_ext_test() {
|
||||
let tmp = match tempfile::TempDir::new() {
|
||||
Ok(t) => t,
|
||||
Err(e) => panic!("Could not create a temp dir: {}", e),
|
||||
Err(e) => panic!("Could not create a temp dir: {e}"),
|
||||
};
|
||||
|
||||
// Create a couple of files
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.txt")) {
|
||||
panic!("Could not create file.txt: {}", err);
|
||||
panic!("Could not create file.txt: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.md")) {
|
||||
panic!("Could not create file.md: {}", err);
|
||||
panic!("Could not create file.md: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.png")) {
|
||||
panic!("Could not create file.png: {}", err);
|
||||
panic!("Could not create file.png: {err}");
|
||||
}
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir")) {
|
||||
panic!("Could not create sub_dir: {}", err);
|
||||
panic!("Could not create sub_dir: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(tmp.path().join("sub_dir/file.png")) {
|
||||
panic!("Could not create sub_dir/file.png: {}", err);
|
||||
panic!("Could not create sub_dir/file.png: {err}");
|
||||
}
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir_exists")) {
|
||||
panic!("Could not create sub_dir_exists: {}", err);
|
||||
panic!("Could not create sub_dir_exists: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(tmp.path().join("sub_dir_exists/file.txt")) {
|
||||
panic!("Could not create sub_dir_exists/file.txt: {}", err);
|
||||
panic!("Could not create sub_dir_exists/file.txt: {err}");
|
||||
}
|
||||
if let Err(err) = symlink(tmp.path().join("file.png"), tmp.path().join("symlink.png")) {
|
||||
panic!("Could not symlink file.png: {}", err);
|
||||
panic!("Could not symlink file.png: {err}");
|
||||
}
|
||||
|
||||
// Create output dir
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("output")) {
|
||||
panic!("Could not create output: {}", err);
|
||||
panic!("Could not create output: {err}");
|
||||
}
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("output/sub_dir_exists")) {
|
||||
panic!("Could not create output/sub_dir_exists: {}", err);
|
||||
panic!("Could not create output/sub_dir_exists: {err}");
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])
|
||||
{
|
||||
panic!("Error while executing the function:\n{:?}", e);
|
||||
panic!("Error while executing the function:\n{e:?}");
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
|
@ -6,7 +6,7 @@ pub(crate) mod toml_ext;
|
||||
use crate::errors::Error;
|
||||
use log::error;
|
||||
use once_cell::sync::Lazy;
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd};
|
||||
use regex::Regex;
|
||||
|
||||
use std::borrow::Cow;
|
||||
@ -77,7 +77,7 @@ pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, us
|
||||
let id_count = id_counter.entry(id.clone()).or_insert(0);
|
||||
let unique_id = match *id_count {
|
||||
0 => id,
|
||||
id_count => format!("{}-{}", id, id_count),
|
||||
id_count => format!("{id}-{id_count}"),
|
||||
};
|
||||
*id_count += 1;
|
||||
unique_id
|
||||
@ -105,7 +105,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
if base.ends_with(".md") {
|
||||
base.replace_range(base.len() - 3.., ".html");
|
||||
}
|
||||
return format!("{}{}", base, dest).into();
|
||||
return format!("{base}{dest}").into();
|
||||
} else {
|
||||
return dest;
|
||||
}
|
||||
@ -121,7 +121,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
.to_str()
|
||||
.expect("utf-8 paths only");
|
||||
if !base.is_empty() {
|
||||
write!(fixed_link, "{}/", base).unwrap();
|
||||
write!(fixed_link, "{base}/").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,38 +161,59 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Start(Tag::Link(link_type, dest, title)) => {
|
||||
Event::Start(Tag::Link(link_type, fix(dest, path), title))
|
||||
}
|
||||
Event::Start(Tag::Image(link_type, dest, title)) => {
|
||||
Event::Start(Tag::Image(link_type, fix(dest, path), title))
|
||||
}
|
||||
Event::Start(Tag::Link {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
}) => Event::Start(Tag::Link {
|
||||
link_type,
|
||||
dest_url: fix(dest_url, path),
|
||||
title,
|
||||
id,
|
||||
}),
|
||||
Event::Start(Tag::Image {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
}) => Event::Start(Tag::Image {
|
||||
link_type,
|
||||
dest_url: fix(dest_url, path),
|
||||
title,
|
||||
id,
|
||||
}),
|
||||
Event::Html(html) => Event::Html(fix_html(html, path)),
|
||||
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
|
||||
_ => event,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
||||
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
render_markdown_with_path(text, curly_quotes, None)
|
||||
pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
|
||||
render_markdown_with_path(text, smart_punctuation, None)
|
||||
}
|
||||
|
||||
pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> {
|
||||
pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(Options::ENABLE_TABLES);
|
||||
opts.insert(Options::ENABLE_FOOTNOTES);
|
||||
opts.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
opts.insert(Options::ENABLE_TASKLISTS);
|
||||
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
||||
if curly_quotes {
|
||||
if smart_punctuation {
|
||||
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||
}
|
||||
Parser::new_ext(text, opts)
|
||||
}
|
||||
|
||||
pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String {
|
||||
pub fn render_markdown_with_path(
|
||||
text: &str,
|
||||
smart_punctuation: bool,
|
||||
path: Option<&Path>,
|
||||
) -> String {
|
||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||
let p = new_cmark_parser(text, curly_quotes);
|
||||
let p = new_cmark_parser(text, smart_punctuation);
|
||||
let events = p
|
||||
.map(clean_codeblock_headers)
|
||||
.map(|event| adjust_links(event, path))
|
||||
@ -212,7 +233,7 @@ fn wrap_tables(event: Event<'_>) -> (Option<Event<'_>>, Option<Event<'_>>) {
|
||||
Some(Event::Html(r#"<div class="table-wrapper">"#.into())),
|
||||
Some(event),
|
||||
),
|
||||
Event::End(Tag::Table(_)) => (Some(event), Some(Event::Html(r#"</div>"#.into()))),
|
||||
Event::End(TagEnd::Table) => (Some(event), Some(Event::Html(r#"</div>"#.into()))),
|
||||
_ => (Some(event), None),
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
- [Tables](individual/table.md)
|
||||
- [Tasks](individual/task.md)
|
||||
- [Strikethrough](individual/strikethrough.md)
|
||||
- [MathJax](individual/mathjax.md)
|
||||
- [Mixed](individual/mixed.md)
|
||||
- [Languages](languages/README.md)
|
||||
- [Syntax Highlight](languages/highlight.md)
|
||||
|
42
test_book/src/individual/mathjax.md
Normal file
42
test_book/src/individual/mathjax.md
Normal file
@ -0,0 +1,42 @@
|
||||
# MathJax
|
||||
|
||||
Fourier Transform
|
||||
|
||||
\\[
|
||||
\begin{aligned}
|
||||
f(x) &= \int_{-\infty}^{\infty}F(s)(-1)^{ 2xs}ds \\\\
|
||||
F(s) &= \int_{-\infty}^{\infty}f(x)(-1)^{-2xs}dx
|
||||
\end{aligned}
|
||||
\\]
|
||||
|
||||
The kernel can also be written as \\(e^{2i\pi xs}\\) which is more frequently used in literature.
|
||||
|
||||
> Proof that \\(e^{ix} = \cos x + i\sin x\\) a.k.a Euler's Formula:
|
||||
>
|
||||
> \\(
|
||||
\begin{aligned}
|
||||
e^x &= \sum_{n=0}^\infty \frac{x^n}{n!} \implies e^{ix} = \sum_{n=0}^\infty \frac{(ix)^n}{n!} \\\\
|
||||
\cos x &= \sum_{m=0}^\infty \frac{(-1)^m x^{2m}}{(2m)!} = \sum_{m=0}^\infty \frac{(ix)^{2m}}{(2m)!} \\\\
|
||||
\sin x &= \sum_{s=0}^\infty \frac{(-1)^s x^{2s+1}}{(2s+1)!} = \sum_{s=0}^\infty \frac{(ix)^{2s+1}}{i(2s+1)!} \\\\
|
||||
\cos x + i\sin x &= \sum_{l=0}^\infty \frac{(ix)^{2l}}{(2l)!} + \sum_{s=0}^\infty \frac{(ix)^{2s+1}}{(2s+1)!} = \sum_{n=0}^\infty \frac{(ix)^{n}}{n!} \\\\
|
||||
&= e^{ix}
|
||||
\end{aligned}
|
||||
\\)
|
||||
>
|
||||
|
||||
|
||||
Pauli Matrices
|
||||
|
||||
\\[
|
||||
\begin{aligned}
|
||||
\sigma_x &= \begin{pmatrix}
|
||||
1 & 0 \\\\ 0 & 1
|
||||
\end{pmatrix} \\\\
|
||||
\sigma_y &= \begin{pmatrix}
|
||||
0 & -i \\\\ i & 0
|
||||
\end{pmatrix} \\\\
|
||||
\sigma_z &= \begin{pmatrix}
|
||||
1 & 0 \\\\ 0 & -1
|
||||
\end{pmatrix}
|
||||
\end{aligned}
|
||||
\\]
|
@ -28,6 +28,7 @@ This Currently contains following languages
|
||||
- markdown
|
||||
- nginx
|
||||
- nim
|
||||
- nix
|
||||
- objectivec
|
||||
- perl
|
||||
- php
|
||||
|
@ -563,6 +563,15 @@ int main(int argc, const char * argv[]) {
|
||||
|
||||
```
|
||||
|
||||
## nix
|
||||
|
||||
```nix
|
||||
let
|
||||
world = "World!";
|
||||
in
|
||||
"Hello " + world
|
||||
```
|
||||
|
||||
## perl
|
||||
|
||||
```perl
|
||||
|
@ -39,29 +39,27 @@ fn alternate_backend_with_arguments() {
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
/// Get a command which will pipe `stdin` to the provided file.
|
||||
#[cfg(not(windows))]
|
||||
fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
|
||||
let out_file = out_file.as_ref();
|
||||
|
||||
if cfg!(windows) {
|
||||
format!("cmd.exe /c \"type > {}\"", out_file.display())
|
||||
} else {
|
||||
format!("tee {}", out_file.display())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn backends_receive_render_context_via_stdin() {
|
||||
use mdbook::renderer::RenderContext;
|
||||
use std::fs::File;
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("output").tempdir().unwrap();
|
||||
let out_file = temp.path().join("out.txt");
|
||||
let cmd = tee_command(&out_file);
|
||||
let (md, temp) = dummy_book_with_backend("cat-to-file", "renderers/myrenderer", false);
|
||||
|
||||
let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd, false);
|
||||
let renderers = temp.path().join("renderers");
|
||||
fs::create_dir(&renderers).unwrap();
|
||||
rust_exe(
|
||||
&renderers,
|
||||
"myrenderer",
|
||||
r#"fn main() {
|
||||
use std::io::Read;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_to_string(&mut s).unwrap();
|
||||
std::fs::write("out.txt", s).unwrap();
|
||||
}"#,
|
||||
);
|
||||
|
||||
let out_file = temp.path().join("book/out.txt");
|
||||
|
||||
assert!(!out_file.exists());
|
||||
md.build().unwrap();
|
||||
@ -119,13 +117,11 @@ fn dummy_book_with_backend(
|
||||
|
||||
let mut config = Config::default();
|
||||
config
|
||||
.set(format!("output.{}.command", name), command)
|
||||
.set(format!("output.{name}.command"), command)
|
||||
.unwrap();
|
||||
|
||||
if backend_is_optional {
|
||||
config
|
||||
.set(format!("output.{}.optional", name), true)
|
||||
.unwrap();
|
||||
config.set(format!("output.{name}.optional"), true).unwrap();
|
||||
}
|
||||
|
||||
let md = MDBook::init(temp.path())
|
||||
|
@ -18,3 +18,7 @@ css looks, like this {
|
||||
}
|
||||
*/
|
||||
</style>
|
||||
|
||||
Sneaky inline event <script>alert("inline");</script>.
|
||||
|
||||
But regular <b>inline</b> is indexed.
|
||||
|
@ -23,7 +23,7 @@ fn base_mdbook_init_should_create_default_content() {
|
||||
for file in &created_files {
|
||||
let target = temp.path().join(file);
|
||||
println!("{}", target.display());
|
||||
assert!(target.exists(), "{} doesn't exist", file);
|
||||
assert!(target.exists(), "{file} doesn't exist");
|
||||
}
|
||||
|
||||
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
|
||||
@ -59,7 +59,7 @@ fn run_mdbook_init_should_create_content_from_summary() {
|
||||
for file in &created_files {
|
||||
let target = src_dir.join(file);
|
||||
println!("{}", target.display());
|
||||
assert!(target.exists(), "{} doesn't exist", file);
|
||||
assert!(target.exists(), "{file} doesn't exist");
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,8 +73,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||
for file in &created_files {
|
||||
assert!(
|
||||
!temp.path().join(file).exists(),
|
||||
"{} shouldn't exist yet!",
|
||||
file
|
||||
"{file} shouldn't exist yet!"
|
||||
);
|
||||
}
|
||||
|
||||
@ -88,8 +87,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||
let target = temp.path().join(file);
|
||||
assert!(
|
||||
target.exists(),
|
||||
"{} should have been created by `mdbook init`",
|
||||
file
|
||||
"{file} should have been created by `mdbook init`"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -375,10 +375,7 @@ fn able_to_include_playground_files_in_chapters() {
|
||||
|
||||
let second = temp.path().join("book/second.html");
|
||||
|
||||
let playground_strings = &[
|
||||
r#"class="playground""#,
|
||||
r#"println!("Hello World!");"#,
|
||||
];
|
||||
let playground_strings = &[r#"class="playground""#, r#"println!("Hello World!");"#];
|
||||
|
||||
assert_contains_strings(&second, playground_strings);
|
||||
assert_doesnt_contain_strings(&second, &["{{#playground example.rs}}"]);
|
||||
@ -745,6 +742,7 @@ mod search {
|
||||
let index = read_book_index(temp.path());
|
||||
|
||||
let doc_urls = index["doc_urls"].as_array().unwrap();
|
||||
eprintln!("doc_urls={doc_urls:#?}",);
|
||||
let get_doc_ref =
|
||||
|url: &str| -> String { doc_urls.iter().position(|s| s == url).unwrap().to_string() };
|
||||
|
||||
@ -774,7 +772,10 @@ mod search {
|
||||
docs[&summary]["breadcrumbs"],
|
||||
"First Chapter » Includes » Summary"
|
||||
);
|
||||
assert_eq!(docs[&conclusion]["body"], "I put <HTML> in here!");
|
||||
// See note about InlineHtml in search.rs. Ideally the `alert()` part
|
||||
// should not be in the index, but we don't have a way to scrub inline
|
||||
// html.
|
||||
assert_eq!(docs[&conclusion]["body"], "I put <HTML> in here! Sneaky inline event alert(\"inline\");. But regular inline is indexed.");
|
||||
assert_eq!(
|
||||
docs[&no_headers]["breadcrumbs"],
|
||||
"First Chapter » No Headers"
|
||||
|
@ -145,7 +145,7 @@
|
||||
"title": 1
|
||||
},
|
||||
"29": {
|
||||
"body": 3,
|
||||
"body": 10,
|
||||
"breadcrumbs": 2,
|
||||
"title": 1
|
||||
},
|
||||
@ -319,7 +319,7 @@
|
||||
"title": "Some section"
|
||||
},
|
||||
"29": {
|
||||
"body": "I put <HTML> in here!",
|
||||
"body": "I put <HTML> in here! Sneaky inline event alert(\"inline\");. But regular inline is indexed.",
|
||||
"breadcrumbs": "Conclusion » Conclusion",
|
||||
"id": "29",
|
||||
"title": "Conclusion"
|
||||
@ -412,6 +412,54 @@
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"l": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"e": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"r": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"t": {
|
||||
"(": {
|
||||
"\"": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"i": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"n": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"l": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"i": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"n": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
"29": {
|
||||
"tf": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {}
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"n": {
|
||||
"c": {
|
||||
"df": 0,
|
||||
@ -1212,6 +1260,14 @@
|
||||
"26": {
|
||||
"tf": 1.0
|
||||
}
|
||||
},
|
||||
"t": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
"29": {
|
||||
"tf": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1684,10 +1740,13 @@
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"x": {
|
||||
"df": 1,
|
||||
"df": 2,
|
||||
"docs": {
|
||||
"0": {
|
||||
"tf": 1.0
|
||||
},
|
||||
"29": {
|
||||
"tf": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1695,6 +1754,22 @@
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"l": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"i": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"n": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
"29": {
|
||||
"tf": 1.4142135623730951
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"s": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
@ -2359,6 +2434,30 @@
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"g": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"u": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"l": {
|
||||
"a": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"r": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
"29": {
|
||||
"tf": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"l": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
@ -2590,6 +2689,26 @@
|
||||
"n": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"e": {
|
||||
"a": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"k": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"i": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
"29": {
|
||||
"tf": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {}
|
||||
},
|
||||
"i": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
@ -3252,6 +3371,54 @@
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"l": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"e": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"r": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"t": {
|
||||
"(": {
|
||||
"\"": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"i": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"n": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"l": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"i": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"n": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
"29": {
|
||||
"tf": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {}
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"n": {
|
||||
"c": {
|
||||
"df": 0,
|
||||
@ -4130,6 +4297,14 @@
|
||||
"26": {
|
||||
"tf": 1.0
|
||||
}
|
||||
},
|
||||
"t": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
"29": {
|
||||
"tf": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4665,10 +4840,13 @@
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"x": {
|
||||
"df": 1,
|
||||
"df": 2,
|
||||
"docs": {
|
||||
"0": {
|
||||
"tf": 1.0
|
||||
},
|
||||
"29": {
|
||||
"tf": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4676,6 +4854,22 @@
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"l": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"i": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"n": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
"29": {
|
||||
"tf": 1.4142135623730951
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"s": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
@ -5373,6 +5567,30 @@
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"g": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"u": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"l": {
|
||||
"a": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"r": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
"29": {
|
||||
"tf": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"l": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
@ -5610,6 +5828,26 @@
|
||||
"n": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"e": {
|
||||
"a": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"k": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
"i": {
|
||||
"df": 1,
|
||||
"docs": {
|
||||
"29": {
|
||||
"tf": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"df": 0,
|
||||
"docs": {}
|
||||
},
|
||||
"i": {
|
||||
"df": 0,
|
||||
"docs": {},
|
||||
|
Loading…
Reference in New Issue
Block a user