Compare commits

...

No commits in common. "main" and "site-url-0.4.40" have entirely different histories.

726 changed files with 25751 additions and 20978 deletions

View file

@ -1,2 +0,0 @@
[alias]
xtask = "run --manifest-path=crates/xtask/Cargo.toml --"

View file

@ -1,26 +0,0 @@
# Use `git config blame.ignorerevsfile .git-blame-ignore-revs` to make `git blame` ignore the following commits.
# rustfmt
ad0794a0bd692e4f2ff23b85e361889620e93f51
# rustfmt and use_try_shorthand
75bbd55128083897d40c3f5265cc5b1f10314ddb
# rustfmt
382fc4139b96bde3c4b8875b499c720eabc89c6a
# rustfmt
154e0fb3080c6ffc225b0d47b5d835e589789892
# rustfmt
5835da243244bfc5c95c6c6db96f453da4bb5740
# rustfmt
fd9d27e082f5e9eea50e4fa9fa3a22060d02c66b
# rustfmt
1d69ccae4854f13552d452d0bffef95cbff70364
# rustfmt
3688f73052454bf510a5acc85cf55aae450c6e46
# rustfmt
742dbbc91700dce1b7d910bca6b3e10a5ae46b86
# rustfmt 1.38
b88839cc25a6fd1c782101e94318959e8079bb20
# rustfmt 1.40
2f59943c04f0aa204a9238d6a699ba9cc06c88d9
# Rustfmt for 2024
c7b67e363bb9ce3383636ee615e8e761bf185b33

2
.gitattributes vendored
View file

@ -6,5 +6,3 @@
*.ttf binary *.ttf binary
*.otf binary *.otf binary
*.png binary *.png binary
*.eot binary
*.woff2 binary

View file

@ -1,71 +0,0 @@
{
schedule: ['before 5am on the first day of the month'],
// Raise from default of 2 to reduce trickle.
prHourlyLimit: 6,
dependencyDashboard: true,
// Creates PRs if this renovate config file needs updating.
configMigration: true,
ignorePaths: [
'guide/src/for_developers/mdbook-wordcount/',
],
customManagers: [
// Custom manager to extract the version of cargo-semver-checks from the workflow.
{
customType: 'regex',
managerFilePatterns: [
'/^.github.workflows.main.yml$/',
],
matchStrings: [
'cargo-semver-checks.releases.download.v(?<currentValue>\\d+\\.\\d+(\\.\\d+)?)',
],
depNameTemplate: 'cargo-semver-checks',
packageNameTemplate: 'obi1kenobi/cargo-semver-checks',
datasourceTemplate: 'github-releases',
},
],
packageRules: [
// The next two rules disable compatible dependency updates. I wasn't
// able to get Renovate to be able to update Cargo.toml for compatible
// updates only, update all transitive dependencies, and do that all
// in a single PR. Instead, the `update-dependencies.sh` will handle
// that.
{
matchManagers: ['cargo'],
matchUpdateTypes: ['patch'],
enabled: false,
},
{
matchManagers: ['cargo'],
matchCurrentVersion: '>=1.0.0',
matchUpdateTypes: ['minor'],
enabled: false,
},
// Allow minor updates for pre-1.0 dependencies (semver-breaking)
{
matchManagers: ['cargo'],
matchCurrentVersion: '<1.0.0',
matchUpdateTypes: ['minor'],
},
// Allow major updates for stable dependencies (semver-breaking)
{
matchManagers: ['cargo'],
matchCurrentVersion: '>=1.0.0',
matchUpdateTypes: ['major'],
},
// Update cargo-semver-checks when a new version is available.
{
commitMessageTopic: 'cargo-semver-checks',
matchManagers: [
'custom.regex',
],
matchDepNames: [
'cargo-semver-checks',
],
extractVersion: '^v(?<version>\\d+\\.\\d+\\.\\d+)',
schedule: [
'* * * * *',
],
internalChecksFilter: 'strict',
},
]
}

View file

@ -17,20 +17,18 @@ jobs:
matrix: matrix:
include: include:
- target: aarch64-unknown-linux-musl - target: aarch64-unknown-linux-musl
os: ubuntu-22.04 os: ubuntu-20.04
- target: x86_64-unknown-linux-gnu - target: x86_64-unknown-linux-gnu
os: ubuntu-22.04 os: ubuntu-20.04
- target: x86_64-unknown-linux-musl - target: x86_64-unknown-linux-musl
os: ubuntu-22.04 os: ubuntu-20.04
- target: x86_64-apple-darwin - target: x86_64-apple-darwin
os: macos-latest os: macos-latest
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-pc-windows-msvc - target: x86_64-pc-windows-msvc
os: windows-latest os: windows-latest
name: Deploy ${{ matrix.target }} name: Deploy ${{ matrix.target }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
run: ci/install-rust.sh stable ${{ matrix.target }} run: ci/install-rust.sh stable ${{ matrix.target }}
- name: Build asset - name: Build asset
@ -43,26 +41,27 @@ jobs:
name: GitHub Pages name: GitHub Pages
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
- name: Install Rust (rustup) - name: Install Rust (rustup)
run: rustup update stable --no-self-update && rustup default stable run: rustup update stable --no-self-update && rustup default stable
- name: Deploy the User Guide to GitHub Pages using the gh-pages branch - name: Build book
run: ci/publish-guide.sh 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: publish:
name: Publish to crates.io name: Publish to crates.io
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
# Required for OIDC token exchange
id-token: write
environment: publish
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
- name: Install Rust (rustup) - name: Install Rust (rustup)
run: rustup update stable --no-self-update && rustup default stable run: rustup update stable --no-self-update && rustup default stable
- name: Authenticate with crates.io
id: auth
uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4
- name: Publish - name: Publish
env: env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --workspace --no-verify run: cargo publish --no-verify

View file

@ -22,7 +22,7 @@ jobs:
rust: nightly rust: nightly
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
- name: stable x86_64-unknown-linux-musl - name: stable x86_64-unknown-linux-musl
os: ubuntu-22.04 os: ubuntu-20.04
rust: stable rust: stable
target: x86_64-unknown-linux-musl target: x86_64-unknown-linux-musl
- name: stable x86_64 macos - name: stable x86_64 macos
@ -38,24 +38,24 @@ jobs:
rust: stable rust: stable
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
- name: msrv - name: msrv
os: ubuntu-22.04 os: ubuntu-20.04
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml # sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
rust: 1.88.0 rust: 1.74.0
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
name: ${{ matrix.name }} name: ${{ matrix.name }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
run: bash ci/install-rust.sh ${{ matrix.rust }} ${{ matrix.target }} run: bash ci/install-rust.sh ${{ matrix.rust }} ${{ matrix.target }}
- name: Build and run tests - name: Build and run tests
run: cargo test --workspace --locked --target ${{ matrix.target }} run: cargo test --locked --target ${{ matrix.target }}
- name: Test no default - name: Test no default
run: cargo test --workspace --no-default-features --target ${{ matrix.target }} run: cargo test --no-default-features --target ${{ matrix.target }}
aarch64-cross-builds: aarch64-cross-builds:
runs-on: ubuntu-22.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
run: bash ci/install-rust.sh stable aarch64-unknown-linux-musl run: bash ci/install-rust.sh stable aarch64-unknown-linux-musl
- name: Build - name: Build
@ -65,65 +65,11 @@ jobs:
name: Rustfmt name: Rustfmt
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
run: rustup update stable && rustup default stable && rustup component add rustfmt run: rustup update stable && rustup default stable && rustup component add rustfmt
- run: cargo fmt --check - run: cargo fmt --check
gui:
name: GUI tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
- name: Install npm
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- name: Install browser-ui-test
run: npm install
- name: Run eslint
run: npm run lint
- name: Build and run tests (+ GUI)
run: cargo test --locked --target x86_64-unknown-linux-gnu --test gui
# Ensure there are no clippy warnings
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
- run: rustup component add clippy
- run: cargo clippy --workspace --all-targets --no-deps -- -D warnings
docs:
name: Check API docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
- name: Ensure intradoc links are valid
run: cargo doc --workspace --document-private-items --no-deps
env:
RUSTDOCFLAGS: -D warnings
check-version-bump:
name: Check version bump
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: rustup update stable && rustup default stable
- name: Install cargo-semver-checks
run: |
mkdir installed-bins
curl -Lf https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.48.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz \
| tar -xz --directory=./installed-bins
echo `pwd`/installed-bins >> $GITHUB_PATH
- run: cargo semver-checks --workspace
# The success job is here to consolidate the total success/failure state of # 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 # 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 # rule which prevents merges unless all other jobs are passing. This makes
@ -135,11 +81,6 @@ jobs:
needs: needs:
- test - test
- rustfmt - rustfmt
- aarch64-cross-builds
- gui
- clippy
- docs
- check-version-bump
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}' - run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'

View file

@ -1,21 +0,0 @@
name: Update dependencies
on:
schedule:
- cron: '0 0 1 * *'
workflow_dispatch:
jobs:
update:
name: Update dependencies
runs-on: ubuntu-latest
if: github.repository == 'rust-lang/mdBook'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
- name: Install cargo-edit
run: cargo install cargo-edit --locked
- name: Update dependencies
run: ci/update-dependencies.sh
env:
GH_TOKEN: ${{ github.token }}

8
.gitignore vendored
View file

@ -8,8 +8,7 @@ guide/book
.vscode .vscode
tests/dummy_book/book/ tests/dummy_book/book/
tests/gui/books/*/book/ test_book/book/
tests/testsuite/*/*/book/
# Ignore Jetbrains specific files. # Ignore Jetbrains specific files.
.idea/ .idea/
@ -17,8 +16,3 @@ tests/testsuite/*/*/book/
# Ignore Vim temporary and swap files. # Ignore Vim temporary and swap files.
*.sw? *.sw?
*~ *~
# GUI tests
node_modules
package-lock.json
package.json

View file

@ -1,628 +1,5 @@
# Changelog # Changelog
## mdBook 0.5.3
[v0.5.2...v0.5.3](https://github.com/rust-lang/mdBook/compare/v0.5.2...v0.5.3)
### Changed
- Improve spacing in sidebar section headings.
[#3122](https://github.com/rust-lang/mdBook/pull/3122)
- Updated cargo dependencies.
[#3083](https://github.com/rust-lang/mdBook/pull/3083)
[#3077](https://github.com/rust-lang/mdBook/pull/3077)
[#3058](https://github.com/rust-lang/mdBook/pull/3058)
[#3057](https://github.com/rust-lang/mdBook/pull/3057)
[#3042](https://github.com/rust-lang/mdBook/pull/3042)
[#3045](https://github.com/rust-lang/mdBook/pull/3045)
[#3044](https://github.com/rust-lang/mdBook/pull/3044)
[#3036](https://github.com/rust-lang/mdBook/pull/3036)
[#3022](https://github.com/rust-lang/mdBook/pull/3022)
[#3017](https://github.com/rust-lang/mdBook/pull/3017)
[#3018](https://github.com/rust-lang/mdBook/pull/3018)
[#3019](https://github.com/rust-lang/mdBook/pull/3019)
[#2998](https://github.com/rust-lang/mdBook/pull/2998)
[#2996](https://github.com/rust-lang/mdBook/pull/2996)
### Fixed
- The "current" page highlighting in the sidebar now handles servers that redirect and strip the `.html` extension.
[#3028](https://github.com/rust-lang/mdBook/pull/3028)
- Remove `?highlight=` from URL when highlights are dismissed via clicking.
[#3084](https://github.com/rust-lang/mdBook/pull/3084)
- Fix global keypresses triggering when other elements are in focus.
[#3087](https://github.com/rust-lang/mdBook/pull/3087)
- Fix download URL format for mdBook in CI guide.
[#3073](https://github.com/rust-lang/mdBook/pull/3073)
- Improve error message for invalid Font Awesome icons.
[#3037](https://github.com/rust-lang/mdBook/pull/3037)
- Fix nested admonitions that use wrong header colors.
[#3035](https://github.com/rust-lang/mdBook/pull/3035)
## mdBook 0.5.2
[v0.5.1...v0.5.2](https://github.com/rust-lang/mdBook/compare/v0.5.1...v0.5.2)
### Changed
- Updated Rust crate html5ever to 0.36.0.
[#2970](https://github.com/rust-lang/mdBook/pull/2970)
- Updated cargo dependencies.
[#2969](https://github.com/rust-lang/mdBook/pull/2969)
### Fixed
- Fixed repeated error message when HTML config is invalid in `mdbook serve`.
[#2983](https://github.com/rust-lang/mdBook/pull/2983)
- Fixed sidebar scroll position when heading nav is involved.
[#2982](https://github.com/rust-lang/mdBook/pull/2982)
- Fixed color for rustdoc error messages.
[#2981](https://github.com/rust-lang/mdBook/pull/2981)
- Fixed usage of custom preprocessors with `MDBook::test`.
[#2980](https://github.com/rust-lang/mdBook/pull/2980)
## mdBook 0.5.1
[v0.5.0...v0.5.1](https://github.com/rust-lang/mdBook/compare/v0.5.0...v0.5.1)
### Changed
- Changed the scrollbar background to be transparent.
[#2932](https://github.com/rust-lang/mdBook/pull/2932)
- Ignore invalid top-level environment variable config keys. This allows setting things like `MDBOOK_VERSION` to not cause an error.
[#2952](https://github.com/rust-lang/mdBook/pull/2952)
### Fixed
- Fixed the sidebar heading nav to have the correct nesting levels.
[#2953](https://github.com/rust-lang/mdBook/pull/2953)
- Various Font Awesome fixes and improvements.
[#2951](https://github.com/rust-lang/mdBook/pull/2951)
## mdBook 0.5.0
[v0.4.52...v0.5.0](https://github.com/rust-lang/mdBook/compare/v0.4.52...v0.5.0)
The 0.5.0 release is the next major release of mdBook, containing over 130 PRs since 0.4.52! The primary focus for this release has been an evolution of the Rust APIs to make it easier to maintain, to evolve in a backwards-compatible fashion, to clean up some things that have accumulated over time, and to significantly improve the performance and compile-times.
This release also includes many new features described below.
We have prepared a [0.5 Migration Guide](#05-migration-guide) to help existing authors switch from 0.4.
The final 0.5.0 release only contains the following changes since [0.5.0-beta.2](#mdbook-050-beta2):
- Added error handling to environment config handling. This checks that environment variables starting with `MDBOOK_` are correctly specified instead of silently ignoring. This also fixed being able to replace entire top-level tables like `MDBOOK_OUTPUT`.
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
## 0.5 Migration Guide
The 0.5 release contains several breaking changes from the 0.4 release. Preprocessors and renderers will need to be migrated to continue to work with this release. After updating your configuration, it is recommended to carefully compare and review how your book renders to ensure everything is working correctly.
If you have overridden any of the theme files, you will likely need to update them to match the current version.
See the entries below for [mdBook 0.5.0-alpha.1](#mdbook-050-alpha1), [mdBook 0.5.0-beta.1](#mdbook-050-beta1), and [mdBook 0.5.0-beta.2](#mdbook-050-beta2) for a more complete list of changes and fixes.
The following is a summary of the changes that may require your attention when updating to 0.5:
### Major additions
- Added sidebar heading navigation. This includes the `output.html.sidebar-header-nav` option to disable it.
[#2822](https://github.com/rust-lang/mdBook/pull/2822)
- Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it. See [docs](https://rust-lang.github.io/mdBook/format/markdown.html#definition-lists) for more.
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
- Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it. See [docs](https://rust-lang.github.io/mdBook/format/markdown.html#admonitions) for more.
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
- Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
### Config changes
- Unknown fields in config are now an error.
[#2787](https://github.com/rust-lang/mdBook/pull/2787)
[#2801](https://github.com/rust-lang/mdBook/pull/2801)
- Removed `curly-quotes`, use `output.html.smart-punctuation` instead.
[#2788](https://github.com/rust-lang/mdBook/pull/2788)
- Removed `output.html.copy-fonts`. The default fonts are now always copied unless you override the `theme/fonts/fonts.css` file.
[#2790](https://github.com/rust-lang/mdBook/pull/2790)
- If the `command` path for a renderer or preprocessor is relative, it is now always relative to the book root.
[#2792](https://github.com/rust-lang/mdBook/pull/2792)
[#2796](https://github.com/rust-lang/mdBook/pull/2796)
- Added the `optional` field for preprocessors. The default is `false`, so this also means it is an error by default if the preprocessor is missing.
[#2797](https://github.com/rust-lang/mdBook/pull/2797)
- `output.html.smart-punctuation` is now `true` by default.
[#2810](https://github.com/rust-lang/mdBook/pull/2810)
- `output.html.hash-files` is now `true` by default.
[#2820](https://github.com/rust-lang/mdBook/pull/2820)
- Removed support for google-analytics. Use a theme extension (like `head.hbs`) if you need to continue to support this.
[#2776](https://github.com/rust-lang/mdBook/pull/2776)
- Removed the `book.multilingual` field. This was never used.
[#2775](https://github.com/rust-lang/mdBook/pull/2775)
- Removed the very old legacy config support. Warnings have been displayed in previous versions on how to migrate.
[#2783](https://github.com/rust-lang/mdBook/pull/2783)
- Top-level config values set from the environment like `MDBOOK_BOOK` now *replace* the contents of the top-level table instead of merging into it.
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
- Invalid environment variables are now rejected. Previously unknown keys like `MDBOOK_FOO` would be ignored, or keys or invalid values inside objects like the `[book]` table would be ignored.
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
### Theme changes
- Replaced the `{{#previous}}` and `{{#next}}` handlebars helpers with simple objects that contain the previous and next values.
[#2794](https://github.com/rust-lang/mdBook/pull/2794)
- Removed the `{{theme_option}}` handlebars helper. It has not been used for a while.
[#2795](https://github.com/rust-lang/mdBook/pull/2795)
### Rendering changes
- Updated to a newer version of `pulldown-cmark`. This brings a large number of fixes to markdown processing.
[#2401](https://github.com/rust-lang/mdBook/pull/2401)
- The font-awesome font is no longer loaded as a font. Instead, the corresponding SVG is embedded in the output for the corresponding `<i>` tags. Additionally, a handlebars helper has been added for the `hbs` files. This also updates the version from 4.7.0 to 6.2.0, which means some of the icon names and styles have changed. Most of the free icons are in the "solid" set. See the [free icon set](https://fontawesome.com/v6/search) for the available icons.
[#1330](https://github.com/rust-lang/mdBook/pull/1330)
- Changed all internal HTML IDs to have an `mdbook-` prefix. This helps avoid namespace conflicts with header IDs.
[#2808](https://github.com/rust-lang/mdBook/pull/2808)
- There is a new internal HTML rendering pipeline. This is primarily intended to give mdBook more flexibility in generating its HTML output. This resulted in some small changes to the HTML structure. HTML parsing may now be more strict than before.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
- Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
- Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it.
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
- Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it.
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
- Header ID generation has some minor changes to bring the ID generation closer to other tools and sites:
- IDs now use Unicode lowercase instead of ASCII lowercase.
[#2922](https://github.com/rust-lang/mdBook/pull/2922)
- Headers that start or end with HTML characters like `<`, `&`, or `>` now replace those characters in the link ID with `-` instead of being stripped.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
- Headers are no longer modified if the tag is manually written HTML.
[#2913](https://github.com/rust-lang/mdBook/pull/2913)
### CLI changes
- Removed the `--dest-dir` option to `mdbook test`. It was unused since `mdbook test` does not generate output.
[#2805](https://github.com/rust-lang/mdBook/pull/2805)
- Changed CLI `--dest-dir` to be relative to the current directory, not the book root.
[#2806](https://github.com/rust-lang/mdBook/pull/2806)
### Rust API
- The Rust API has been split into several crates ([#2766](https://github.com/rust-lang/mdBook/pull/2766)). In summary, the different crates are:
- `mdbook` — The CLI binary.
- [`mdbook-driver`](https://docs.rs/mdbook-driver/latest/mdbook_driver/) — The high-level library for running mdBook, primarily through the `MDBook` type. If you are driving mdBook programmatically, this is the crate you want.
- [`mdbook-preprocessor`](https://docs.rs/mdbook-preprocessor/latest/mdbook_preprocessor/) — Support for implementing preprocessors. If you have a preprocessor, then this is the crate you should depend on.
- [`mdbook-renderer`](https://docs.rs/mdbook-renderer/latest/mdbook_renderer/) — Support for implementing renderers. If you have a custom renderer, this is the crate you should depend on.
- [`mdbook-markdown`](https://docs.rs/mdbook-markdown/latest/mdbook_markdown/) — The Markdown renderer. If you are processing markdown, this is the crate you should depend on. This is essentially a thin wrapper around `pulldown-cmark`, and re-exports that crate so that you can ensure the version stays in sync with mdBook.
- [`mdbook-summary`](https://docs.rs/mdbook-summary/latest/mdbook_summary/) — The `SUMMARY.md` parser.
- [`mdbook-html`](https://docs.rs/mdbook-html/latest/mdbook_html/) — The HTML renderer.
- [`mdbook-core`](https://docs.rs/mdbook-core/latest/mdbook_core/) — An internal library that is used by the other crates for shared types. You should not depend on this crate directly since types from this crate are re-exported from the other crates as appropriate.
- Changes to `Config`:
- [`Config::get`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.get) is now generic over the return value, using `serde` to deserialize the value. It also returns a `Result` to handle deserialization errors. [#2773](https://github.com/rust-lang/mdBook/pull/2773)
- [`Config::set`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.set) now validates that the config keys and values are valid.
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
- [`Config::update_from_env`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.update_from_env) now returns a `Result` to indicate any errors.
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
- Removed `Config::get_deserialized`. Use `Config::get` instead.
- Removed `Config::get_deserialized_opt`. Use `Config::get` instead.
- Removed `Config::get_mut`. Use `Config::set` instead.
- Removed deprecated `Config::get_deserialized_opt`. Use `Config::get` instead.
- Removed `Config::get_renderer`. Use `Config::get` instead.
- Removed `Config::get_preprocessor`. Use `Config::get` instead.
- Public types have been switch to use the `#[non_exhaustive]` attribute to help allow them to change in a backwards-compatible way.
[#2779](https://github.com/rust-lang/mdBook/pull/2779)
[#2823](https://github.com/rust-lang/mdBook/pull/2823)
- Changed `MDBook` `with_renderer`/`with_preprocessor` to overwrite the entry if an extension of the same name is already loaded. This allows the caller to replace an entry.
[#2802](https://github.com/rust-lang/mdBook/pull/2802)
- Added `MarkdownOptions` struct to specify settings for markdown rendering for `mdbook_markdown::new_cmark_parser`.
[#2809](https://github.cocm/rust-lang/mdBook/pull/2809)
- Renamed `Book::sections` to `Book::items`.
[#2813](https://github.com/rust-lang/mdBook/pull/2813)
- `mdbook::book::load_book` is now private. Instead, use one of the `MDBook` load functions like `MDBook::load_with_config`.
- Removed `HtmlConfig::smart_punctuation` method, use the field of the same name.
- `CmdPreprocessor::parse_input` moved to `mdbook_preprocessor::parse_input`.
- `Preprocessor::supports_renderer` now returns a `Result<bool>` instead of `bool` to be able to handle errors.
- Most of the types from the `theme` module are now private. The `Theme` struct is still exposed for working with themes.
- Various functions in the `utils::fs` module have been removed, renamed, or reworked.
- Most of the functions in the `utils` module have been moved, removed, or made private.
## mdBook 0.5.0-beta.2
[v0.5.0-beta.1...v0.5.0-beta.2](https://github.com/rust-lang/mdBook/compare/v0.5.0-beta.1...v0.5.0-beta.2)
### Added
- Added a warning when a Font Awesome icon is missing.
[#2915](https://github.com/rust-lang/mdBook/pull/2915)
- Added some trace logging for event processing.
[#2911](https://github.com/rust-lang/mdBook/pull/2911)
- Added `Config::contains_key`.
[#2910](https://github.com/rust-lang/mdBook/pull/2910)
### Changed
- Heading IDs are now lowercase.
[#2922](https://github.com/rust-lang/mdBook/pull/2922)
- Updated cargo dependencies.
[#2916](https://github.com/rust-lang/mdBook/pull/2916)
- Removed italics for in quotes/comments in code blocks with the `ayu` theme.
[#2904](https://github.com/rust-lang/mdBook/pull/2904)
- Exposed "search" feature from mdbook-driver.
[#2907](https://github.com/rust-lang/mdBook/pull/2907)
### Fixed
- Fixed rust fenced code blocks with an indent.
[#2905](https://github.com/rust-lang/mdBook/pull/2905)
- Headers and `dt` tags are no longer modified if the tag is manually written HTML.
[#2913](https://github.com/rust-lang/mdBook/pull/2913)
- Fixed print page links for internal links to non-chapters.
[#2914](https://github.com/rust-lang/mdBook/pull/2914)
- Better handling for unbalanced HTML tags.
[#2924](https://github.com/rust-lang/mdBook/pull/2924)
- Handle unclosed HTML tags inside a markdown element.
[#2927](https://github.com/rust-lang/mdBook/pull/2927)
- Fixed missing font-awesome icons in the guide.
[#2926](https://github.com/rust-lang/mdBook/pull/2926)
- Hide the sidebar resize indicator when JS isn't available.
[#2923](https://github.com/rust-lang/mdBook/pull/2923)
## mdBook 0.5.0-beta.1
[v0.5.0-alpha.1...v0.5.0-beta.1](https://github.com/rust-lang/mdBook/compare/v0.5.0-alpha.1...v0.5.0-beta.1)
### Changed
- Reworked the look of the header navigation.
[#2898](https://github.com/rust-lang/mdBook/pull/2898)
- Update cargo dependencies.
[#2896](https://github.com/rust-lang/mdBook/pull/2896)
- Improved the heading nav debug.
[#2892](https://github.com/rust-lang/mdBook/pull/2892)
### Fixed
- Fixed error message for config.get deserialization error.
[#2902](https://github.com/rust-lang/mdBook/pull/2902)
- Filter `<mark>` tags from sidebar heading nav.
[#2899](https://github.com/rust-lang/mdBook/pull/2899)
- Avoid divide-by-zero in heading nav computation
[#2891](https://github.com/rust-lang/mdBook/pull/2891)
- Fixed heading nav with folded chapters.
[#2893](https://github.com/rust-lang/mdBook/pull/2893)
## mdBook 0.5.0-alpha.1
[v0.4.52...v0.5.0-alpha.1](https://github.com/rust-lang/mdBook/compare/v0.4.52...v0.5.0-alpha.1)
### Added
- The location of the generated HTML book is now displayed on the console.
[#2729](https://github.com/rust-lang/mdBook/pull/2729)
- ❗ Added the `optional` field for preprocessors. The default is `false`, so this also changes it so that it is an error if the preprocessor is missing.
[#2797](https://github.com/rust-lang/mdBook/pull/2797)
- ❗ Added `MarkdownOptions` struct to specify settings for markdown rendering.
[#2809](https://github.cocm/rust-lang/mdBook/pull/2809)
- Added sidebar heading navigation. This includes the `output.html.sidebar-header-nav` option to disable it.
[#2822](https://github.com/rust-lang/mdBook/pull/2822)
- Added the mdbook version to the guide.
[#2826](https://github.com/rust-lang/mdBook/pull/2826)
- Added `Book::chapters` and `Book::for_each_chapter_mut` to more conveniently iterate over chapters (instead of all items).
[#2838](https://github.com/rust-lang/mdBook/pull/2838)
- ❗ Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it.
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
- ❗ Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it.
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
### Changed
- ❗ The `mdbook` crate has been split into multiple crates.
[#2766](https://github.com/rust-lang/mdBook/pull/2766)
- The minimum Rust version has been updated to 1.88.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
- ❗ `pulldown-cmark` has been upgraded to 0.13.0, bringing a large number of fixes to markdown processing.
[#2401](https://github.com/rust-lang/mdBook/pull/2401)
- ❗ Switched public types to `non_exhaustive` to help allow them to change in a backwards-compatible way.
[#2779](https://github.com/rust-lang/mdBook/pull/2779)
[#2823](https://github.com/rust-lang/mdBook/pull/2823)
- ❗ Unknown fields in config are now an error.
[#2787](https://github.com/rust-lang/mdBook/pull/2787)
[#2801](https://github.com/rust-lang/mdBook/pull/2801)
- ❗ Changed `id_from_content` to be private.
[#2791](https://github.com/rust-lang/mdBook/pull/2791)
- ❗ Changed preprocessor `command` to use paths relative to the book root.
[#2796](https://github.com/rust-lang/mdBook/pull/2796)
- ❗ Replaced the `{{#previous}}` and `{{#next}}` handelbars navigation helpers with objects.
[#2794](https://github.com/rust-lang/mdBook/pull/2794)
- ❗ Use embedded SVG instead of fonts for icons, font-awesome 6.2.
[#1330](https://github.com/rust-lang/mdBook/pull/1330)
- The `book.src` field is no longer serialized if it is the default of "src".
[#2800](https://github.com/rust-lang/mdBook/pull/2800)
- ❗ Changed `MDBook` `with_renderer`/`with_preprocessor` to overwrite the entry if an extension of the same name is already loaded.
[#2802](https://github.com/rust-lang/mdBook/pull/2802)
- ❗ Changed CLI `--dest-dir` to be relative to the current directory, not the book root.
[#2806](https://github.com/rust-lang/mdBook/pull/2806)
- ❗ Changed all internal HTML IDs to have an `mdbook-` prefix. This helps avoid namespace conflicts with header IDs.
[#2808](https://github.com/rust-lang/mdBook/pull/2808)
- ❗ `output.html.smart-punctuation` is now `true` by default.
[#2810](https://github.com/rust-lang/mdBook/pull/2810)
- ❗ Renamed `Book::sections` to `Book::items`.
[#2813](https://github.com/rust-lang/mdBook/pull/2813)
- ❗ `output.html.hash-files` is now `true` by default.
[#2820](https://github.com/rust-lang/mdBook/pull/2820)
- Switched from `log` to `tracing`.
[#2829](https://github.com/rust-lang/mdBook/pull/2829)
- ❗ Rewrote the HTML rendering pipeline.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
- ❗ Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
- ❗ Moved theme copy to the Theme type and reduced visibility.
[#2857](https://github.com/rust-lang/mdBook/pull/2857)
- ❗ Cleaned up some fs-related utilities.
[#2856](https://github.com/rust-lang/mdBook/pull/2856)
- ❗ Moved `get_404_output_file` to `HtmlConfig`.
[#2855](https://github.com/rust-lang/mdBook/pull/2855)
- ❗ Moved `take_lines` functions to `mdbook-driver` and made private.
[#2854](https://github.com/rust-lang/mdBook/pull/2854)
- Updated dependencies.
[#2793](https://github.com/rust-lang/mdBook/pull/2793)
[#2869](https://github.com/rust-lang/mdBook/pull/2869)
### Removed
- ❗ Removed `toml` as a public dependency.
[#2773](https://github.com/rust-lang/mdBook/pull/2773)
- ❗ Removed the `book.multilingual` field. This was never used.
[#2775](https://github.com/rust-lang/mdBook/pull/2775)
- ❗ Removed support for google-analytics.
[#2776](https://github.com/rust-lang/mdBook/pull/2776)
- ❗ Removed the very old legacy config support.
[#2783](https://github.com/rust-lang/mdBook/pull/2783)
- ❗ Removed `curly-quotes`, use `output.html.smart-punctuation` instead.
[#2788](https://github.com/rust-lang/mdBook/pull/2788)
- Removed old warning about `book.json`.
[#2789](https://github.com/rust-lang/mdBook/pull/2789)
- ❗ Removed `output.html.copy-fonts`. The default fonts are now always copied unless you override the `theme/fonts/fonts.css` file.
[#2790](https://github.com/rust-lang/mdBook/pull/2790)
- ❗ Removed legacy relative renderer command paths. Relative renderer command paths now must always be relative to the book root.
[#2792](https://github.com/rust-lang/mdBook/pull/2792)
- ❗ Removed the `{{theme_option}}` handlebars helper. It has not been used for a while.
[#2795](https://github.com/rust-lang/mdBook/pull/2795)
- ❗ Removed the `--dest-dir` option to `mdbook test`.
[#2805](https://github.com/rust-lang/mdBook/pull/2805)
### Fixed
- Fixed handling of multiple footnotes in a row.
[#2807](https://github.com/rust-lang/mdBook/pull/2807)
- Fixed ID collisions when the numeric suffix gets used.
[#2846](https://github.com/rust-lang/mdBook/pull/2846)
- Fixed missing css vars for no-js dark mode.
[#2850](https://github.com/rust-lang/mdBook/pull/2850)
## mdBook 0.4.52
[v0.4.51...v0.4.52](https://github.com/rust-lang/mdBook/compare/v0.4.51...v0.4.52)
**Note:** If you have a custom `index.hbs` theme file, it is recommended that you update it to the latest version to pick up the fixes in this release.
### Added
- Added the ability to redirect `#` HTML fragments using the existing `output.html.redirect` table.
[#2747](https://github.com/rust-lang/mdBook/pull/2747)
- Added the `rel="edit"` attribute to the edit page button.
[#2702](https://github.com/rust-lang/mdBook/pull/2702)
### Changed
- The search index is now only loaded when the search input is opened instead of always being loaded.
[#2553](https://github.com/rust-lang/mdBook/pull/2553)
[#2735](https://github.com/rust-lang/mdBook/pull/2735)
- The `mdbook serve` command has switched its underlying server library from warp to axum.
[#2748](https://github.com/rust-lang/mdBook/pull/2748)
- Updated dependencies.
[#2752](https://github.com/rust-lang/mdBook/pull/2752)
### Fixed
- The sidebar is now set to `display:none` when it is hidden in order to prevent the browser's search from thinking the sidebar's text is visible.
[#2725](https://github.com/rust-lang/mdBook/pull/2725)
- Fixed search index URL not updating correctly when `hash-files` is enabled.
[#2742](https://github.com/rust-lang/mdBook/pull/2742)
[#2746](https://github.com/rust-lang/mdBook/pull/2746)
- Fixed several sidebar animation bugs, particularly when manually resizing.
[#2750](https://github.com/rust-lang/mdBook/pull/2750)
## mdBook 0.4.51
[v0.4.50...v0.4.51](https://github.com/rust-lang/mdBook/compare/v0.4.50...v0.4.51)
### Fixed
- Fixed regression that broke the `S` search hotkey.
[#2713](https://github.com/rust-lang/mdBook/pull/2713)
## mdBook 0.4.50
[v0.4.49...v0.4.50](https://github.com/rust-lang/mdBook/compare/v0.4.49...v0.4.50)
### Added
- Added a keyboard shortcut help popup when pressing `?`.
[#2608](https://github.com/rust-lang/mdBook/pull/2608)
### Changed
- Changed the look of the sidebar resize handle to match the new rustdoc format.
[#2691](https://github.com/rust-lang/mdBook/pull/2691)
- `/` can now be used to open the search bar.
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
- Pressing enter from the search bar will navigate to the first entry.
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
- Updated `opener` to drop some dependencies.
[#2709](https://github.com/rust-lang/mdBook/pull/2709)
- Updated dependencies, MSRV raised to 1.82.
[#2711](https://github.com/rust-lang/mdBook/pull/2711)
### Fixed
- Fixed uncaught exception when pressing down when there are no search results.
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
- Fixed syntax highlighting of Rust code in the ACE editor.
[#2710](https://github.com/rust-lang/mdBook/pull/2710)
## mdBook 0.4.49
[v0.4.48...v0.4.49](https://github.com/rust-lang/mdBook/compare/v0.4.48...v0.4.49)
### Added
- Added a warning on unused fields in the root of `book.toml`.
[#2622](https://github.com/rust-lang/mdBook/pull/2622)
### Changed
- Updated dependencies.
[#2650](https://github.com/rust-lang/mdBook/pull/2650)
[#2688](https://github.com/rust-lang/mdBook/pull/2688)
- Updated minimum Rust version to 1.81.
[#2688](https://github.com/rust-lang/mdBook/pull/2688)
- The unused `book.multilingual` field is no longer serialized, or shown in `mdbook init`.
[#2689](https://github.com/rust-lang/mdBook/pull/2689)
- Speed up search index loading by using `JSON.parse` instead of parsing JavaScript.
[#2633](https://github.com/rust-lang/mdBook/pull/2633)
### Fixed
- Search highlighting will not try to highlight in SVG `<text>` elements because it breaks the element.
[#2668](https://github.com/rust-lang/mdBook/pull/2668)
- Fixed scrolling of the sidebar when a search highlight term is in the URL.
[#2675](https://github.com/rust-lang/mdBook/pull/2675)
- Fixed issues when multiple footnote definitions use the same ID. Now, only one definition is used, and a warning is displayed.
[#2681](https://github.com/rust-lang/mdBook/pull/2681)
- The sidebar is now restricted to 80% of the viewport width to make it possible to collapse it when the viewport is very narrow.
[#2679](https://github.com/rust-lang/mdBook/pull/2679)
## mdBook 0.4.48
[v0.4.47...v0.4.48](https://github.com/rust-lang/mdBook/compare/v0.4.47...v0.4.48)
### Added
- Footnotes now have back-reference links. These links bring the reader back to the original location. As part of this change, footnotes are now only rendered at the bottom of the page. This also includes some styling updates and fixes for footnote rendering.
[#2626](https://github.com/rust-lang/mdBook/pull/2626)
- Added an "Auto" theme selection option which will default to the system-preferred mode. This will also automatically switch when the system changes the preferred mode.
[#2576](https://github.com/rust-lang/mdBook/pull/2576)
### Changed
- The `searchindex.json` file has been removed; only the `searchindex.js` file will be generated.
[#2552](https://github.com/rust-lang/mdBook/pull/2552)
- Updated Javascript code to use eslint.
[#2554](https://github.com/rust-lang/mdBook/pull/2554)
- An error is generated if there are duplicate files in `SUMMARY.md`.
[#2613](https://github.com/rust-lang/mdBook/pull/2613)
## mdBook 0.4.47
[v0.4.46...v0.4.47](https://github.com/rust-lang/mdBook/compare/v0.4.46...v0.4.47)
### Fixed
- Fixed search not showing up in sub-directories.
[#2586](https://github.com/rust-lang/mdBook/pull/2586)
## mdBook 0.4.46
[v0.4.45...v0.4.46](https://github.com/rust-lang/mdBook/compare/v0.4.45...v0.4.46)
### Changed
- The `output.html.hash-files` config option has been added to add hashes to static filenames to bust any caches when a book is updated. `{{resource}}` template tags have been added so that links can be properly generated to those files.
[#1368](https://github.com/rust-lang/mdBook/pull/1368)
### Fixed
- Playground links for Rust 2024 now set the edition correctly.
[#2557](https://github.com/rust-lang/mdBook/pull/2557)
## mdBook 0.4.45
[v0.4.44...v0.4.45](https://github.com/rust-lang/mdBook/compare/v0.4.44...v0.4.45)
### Changed
- Added context to error message when rustdoc is not found.
[#2545](https://github.com/rust-lang/mdBook/pull/2545)
- Slightly changed the styling rules around margins of footnotes.
[#2524](https://github.com/rust-lang/mdBook/pull/2524)
### Fixed
- Fixed an issue where it would panic if a source_path is not set.
[#2550](https://github.com/rust-lang/mdBook/pull/2550)
## mdBook 0.4.44
[v0.4.43...v0.4.44](https://github.com/rust-lang/mdBook/compare/v0.4.43...v0.4.44)
### Added
- Added pre-built aarch64-apple-darwin binaries to the releases.
[#2500](https://github.com/rust-lang/mdBook/pull/2500)
- `mdbook clean` now shows a summary of what it did.
[#2458](https://github.com/rust-lang/mdBook/pull/2458)
- Added the `output.html.search.chapter` config setting to disable search indexing of individual chapters.
[#2533](https://github.com/rust-lang/mdBook/pull/2533)
### Fixed
- Fixed auto-scrolling the side-bar when loading a page with a `#` fragment URL.
[#2517](https://github.com/rust-lang/mdBook/pull/2517)
- Fixed display of sidebar when javascript is disabled.
[#2529](https://github.com/rust-lang/mdBook/pull/2529)
- Fixed the sidebar visibility getting out of sync with the button.
[#2532](https://github.com/rust-lang/mdBook/pull/2532)
### Changed
- ❗ Rust code block hidden lines now follow the same logic as rustdoc. This requires a space after the `#` symbol.
[#2530](https://github.com/rust-lang/mdBook/pull/2530)
- ❗ Updated the Linux pre-built binaries which requires a newer version of glibc (2.34).
[#2523](https://github.com/rust-lang/mdBook/pull/2523)
- Updated dependencies
[#2538](https://github.com/rust-lang/mdBook/pull/2538)
[#2539](https://github.com/rust-lang/mdBook/pull/2539)
## mdBook 0.4.43
[v0.4.42...v0.4.43](https://github.com/rust-lang/mdBook/compare/v0.4.42...v0.4.43)
### Fixed
- Fixed setting the title in `mdbook init` when no git user is configured.
[#2486](https://github.com/rust-lang/mdBook/pull/2486)
### Changed
- The Rust 2024 edition no longer needs `-Zunstable-options`.
[#2495](https://github.com/rust-lang/mdBook/pull/2495)
## mdBook 0.4.42
[v0.4.41...v0.4.42](https://github.com/rust-lang/mdBook/compare/v0.4.41...v0.4.42)
### Fixed
- Fixed chapter list folding.
[#2473](https://github.com/rust-lang/mdBook/pull/2473)
## mdBook 0.4.41
[v0.4.40...v0.4.41](https://github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)
**Note:** If you have a custom `index.hbs` theme file, you will need to update it to the latest version.
### Added
- Added preliminary support for Rust 2024 edition.
[#2398](https://github.com/rust-lang/mdBook/pull/2398)
- Added a full example of the remove-emphasis preprocessor.
[#2464](https://github.com/rust-lang/mdBook/pull/2464)
### Changed
- Adjusted styling of clipboard/play icons.
[#2421](https://github.com/rust-lang/mdBook/pull/2421)
- Updated to handlebars v6.
[#2416](https://github.com/rust-lang/mdBook/pull/2416)
- Attr and section rules now have specific code highlighting.
[#2448](https://github.com/rust-lang/mdBook/pull/2448)
- The sidebar is now loaded from a common file, significantly reducing the book size when there are many chapters.
[#2414](https://github.com/rust-lang/mdBook/pull/2414)
- Updated dependencies.
[#2470](https://github.com/rust-lang/mdBook/pull/2470)
### Fixed
- Improved theme support when JavaScript is disabled.
[#2454](https://github.com/rust-lang/mdBook/pull/2454)
- Fixed broken themes when localStorage has an invalid theme id.
[#2463](https://github.com/rust-lang/mdBook/pull/2463)
- Adjusted the line-height of superscripts (and footnotes) to avoid adding extra space between lines.
[#2465](https://github.com/rust-lang/mdBook/pull/2465)
## mdBook 0.4.40 ## mdBook 0.4.40
[v0.4.39...v0.4.40](https://github.com/rust-lang/mdBook/compare/v0.4.39...v0.4.40) [v0.4.39...v0.4.40](https://github.com/rust-lang/mdBook/compare/v0.4.39...v0.4.40)

View file

@ -7,7 +7,7 @@ If you have come here to learn how to contribute to mdBook, we have some tips fo
First of all, don't hesitate to ask questions! First of all, don't hesitate to ask questions!
Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple. Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple.
## Issue assignment ### Issue assignment
**:warning: Important :warning:** **:warning: Important :warning:**
@ -16,7 +16,7 @@ The current PR backlog is beyond what we can process at this time.
Only issues that have an [`E-Help-wanted`](https://github.com/rust-lang/mdBook/labels/E-Help-wanted) or [`Feature accepted`](https://github.com/rust-lang/mdBook/labels/Feature%20accepted) label will likely receive reviews. Only issues that have an [`E-Help-wanted`](https://github.com/rust-lang/mdBook/labels/E-Help-wanted) or [`Feature accepted`](https://github.com/rust-lang/mdBook/labels/Feature%20accepted) label will likely receive reviews.
If there isn't already an open issue for what you want to work on, please open one first to see if it is something we would be available to review. If there isn't already an open issue for what you want to work on, please open one first to see if it is something we would be available to review.
## Issues to work on ### Issues to work on
If you are starting out, you might be interested in the If you are starting out, you might be interested in the
[E-Easy issues](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy). [E-Easy issues](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
@ -41,7 +41,7 @@ Issues on the issue tracker are categorized with the following labels:
- **S**-prefixed labels show the status of the issue - **S**-prefixed labels show the status of the issue
- **C**-prefixed labels show the category of issue - **C**-prefixed labels show the category of issue
## Building mdBook ### Building mdBook
mdBook builds on stable Rust, if you want to build mdBook from source, here are the steps to follow: mdBook builds on stable Rust, if you want to build mdBook from source, here are the steps to follow:
@ -56,11 +56,11 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
The resulting binary can be found in `mdBook/target/debug/` under the name `mdbook` or `mdbook.exe`. The resulting binary can be found in `mdBook/target/debug/` under the name `mdbook` or `mdbook.exe`.
## Code quality ### Code Quality
We love code quality and Rust has some excellent tools to assist you with contributions. We love code quality and Rust has some excellent tools to assist you with contributions.
### Formatting code with rustfmt #### Formatting Code with rustfmt
Before you make your Pull Request to the project, please run it through the `rustfmt` utility. Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
This will ensure we have good quality source code that is better for us all to maintain. This will ensure we have good quality source code that is better for us all to maintain.
@ -84,7 +84,8 @@ The quick guide is
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang/rustfmt) For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang/rustfmt)
### Finding issues with clippy
#### Finding Issues with Clippy
[Clippy](https://doc.rust-lang.org/clippy/) is a code analyser/linter detecting mistakes, and therefore helps to improve your code. [Clippy](https://doc.rust-lang.org/clippy/) is a code analyser/linter detecting mistakes, and therefore helps to improve your code.
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will help us maintain awesome code. Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will help us maintain awesome code.
@ -98,7 +99,7 @@ Like formatting your code with `rustfmt`, running clippy regularly and before yo
cargo clippy cargo clippy
``` ```
## Change requirements ### Change requirements
Please consider the following when making a change: Please consider the following when making a change:
@ -123,34 +124,7 @@ Please consider the following when making a change:
* Check out the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) for guidelines on designing the API. * Check out the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) for guidelines on designing the API.
## Tests ### Making a pull-request
The main test harness is described in the [testsuite documentation](tests/testsuite/README.md). There are several different commands to run different kinds of tests:
- `cargo test --workspace` — This runs all of the unit and integration tests, except for the GUI tests.
- `cargo test --test gui` — This runs the [GUI test harness](#browser-compatibility-and-testing). This does not get run automatically due to its extra requirements.
- `npm run lint` — [Checks the `.js` files](#checking-changes-in-js-files)
- `cargo test --workspace --no-default-features` — Testing without default features helps check that all feature checks are implemented correctly.
- `cargo clippy --workspace --all-targets --no-deps -- -D warnings` — This makes sure that there are no clippy warnings.
- `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --document-private-items --no-deps` — This verifies that there aren't any rustdoc warnings.
- `cargo fmt --check` — Verifies that everything is formatted correctly.
- `cargo +stable semver-checks` — Verifies that no SemVer breaking changes have been made. You must install [`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks) first.
To help simplify running all these commands, you can run the following cargo command:
```sh
cargo xtask test-all
```
It is useful to run all tests before submitting a PR. While developing I recommend to run some subset of that command based on what you are working on. There are individual arguments for each one. For example:
```sh
cargo xtask test-workspace clippy doc eslint fmt gui semver-checks
```
While developing, remove any of those arguments that are not relevant to what you are changing, or are really slow.
## Making a pull-request
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub. When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
One of the core maintainers will then approve the changes or request some changes before it gets merged. One of the core maintainers will then approve the changes or request some changes before it gets merged.
@ -164,43 +138,8 @@ We generally strive to keep mdBook compatible with a relatively recent browser o
That is, supporting Chrome, Safari, Firefox, Edge on Windows, macOS, Linux, iOS, and Android. That is, supporting Chrome, Safari, Firefox, Edge on Windows, macOS, Linux, iOS, and Android.
If possible, do your best to avoid breaking older browser releases. If possible, do your best to avoid breaking older browser releases.
GUI tests are checked with the GUI testsuite. To run it, you need to install `npm` first. Then run: Any change to the HTML or styling is encouraged to manually check on as many browsers and platforms that you can.
Unfortunately at this time we don't have any automated UI or browser testing, so your assistance in testing is appreciated.
```
cargo test --test gui
```
If you want to only run some tests, you can filter them by passing (part of) their name:
```
cargo test --test gui -- search
```
The first time, it'll fail and ask you to install the `browser-ui-test` package. Install it with the provided
command then re-run the tests.
If you want to disable the headless mode, use the `--disable-headless-test` option:
```
cargo test --test gui -- --disable-headless-test
```
The GUI tests are in the directory `tests/gui` in text files with the `.goml` extension. The books that the tests use are located in the `tests/gui/books` directory. These tests are run using a `node.js` framework called `browser-ui-test`. You can find documentation for this language on its [repository](https://github.com/GuillaumeGomez/browser-UI-test/blob/master/goml-script.md).
### Checking changes in `.js` files
The `.js` files source code is checked using [`eslint`](https://eslint.org/). This is a linter (just like `clippy` in Rust)
for the Javascript language. You can install it with `npm` by running the following command:
```
npm install
```
Then you can run it using:
```
npm run lint
```
## Updating highlight.js ## Updating highlight.js
@ -213,24 +152,20 @@ The following are instructions for updating [highlight.js](https://highlightjs.o
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. 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. 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. 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.
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [syntax GUI test](https://github.com/rust-lang/mdBook/tree/master/tests/gui/books/highlighting) contains a chapter with many languages to examine. Update the test (`highlighting.goml`) to add any new languages. 1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [test_book](https://github.com/rust-lang/mdBook/tree/master/test_book) contains a chapter with many languages to examine.
## Publishing new releases ## Publishing new releases
Instructions for mdBook maintainers to publish a new release: Instructions for mdBook maintainers to publish a new release:
1. Create a PR that bumps the version and updates the changelog: 1. Create a PR to update the version and update the CHANGELOG:
1. `git fetch upstream` 1. Update the version in `Cargo.toml`
2. `git checkout -B bump-version upstream/master && git branch --set-upstream-to=origin/bump-version` 2. Run `cargo test` to verify that everything is passing, and to update `Cargo.lock`.
3. `cargo xtask bump <BUMP>` 3. Double-check for any SemVer breaking changes.
- This will update the version of all the crates. Try [`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks), though beware that the current version of mdBook isn't properly adhering to SemVer due to the lack of `#[non_exhaustive]` and other issues. See https://github.com/rust-lang/mdBook/issues/1835.
- `cargo set-version` must first be installed with `cargo install cargo-edit`. 4. Update `CHANGELOG.md` with any changes that users may be interested in.
- Replace `<BUMP>` with the kind of bump (patch, alpha, etc.) 5. Update `continuous-integration.md` to update the version number for the installation instructions.
4. `cargo xtask changelog` 6. Commit the changes, and open a PR.
- This will update `CHANGELOG.md` to add a list of all changes at the top. You will need to move those into the appropriate categories. Most changes that are generally not relevant to a user should be removed. Rewrite the descriptions so that a user can reasonably figure out what it means.
5. `git add --update .`
6. `git commit`
7. `git push`
2. After the PR has been merged, create a release in GitHub. This can either be done in the GitHub web UI, or on the command-line: 2. After the PR has been merged, create a release in GitHub. This can either be done in the GitHub web UI, or on the command-line:
```bash ```bash
MDBOOK_VERS="`cargo read-manifest | jq -r .version`" ; \ MDBOOK_VERS="`cargo read-manifest | jq -r .version`" ; \

2297
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,133 +1,70 @@
[workspace]
members = [
".",
"crates/*",
"examples/remove-emphasis/mdbook-remove-emphasis", "guide/guide-helper",
]
[workspace.lints.clippy]
all = { level = "allow", priority = -2 }
correctness = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
exhaustive_enums = "warn"
exhaustive_structs = "warn"
manual_non_exhaustive = "warn"
[workspace.lints.rust]
missing_docs = "warn"
rust_2018_idioms = "warn"
unreachable_pub = "warn"
[workspace.package]
edition = "2024"
license = "MPL-2.0"
repository = "https://github.com/rust-lang/mdBook"
rust-version = "1.88.0" # Keep in sync with installation.md and .github/workflows/main.yml
[workspace.dependencies]
anyhow = "1.0.102"
axum = "0.8.9"
clap = { version = "4.6.1", features = ["cargo", "wrap_help"] }
clap_complete = "4.6.5"
ego-tree = "0.11.0"
elasticlunr-rs = "3.0.2"
font-awesome-as-a-crate = "0.3.1"
futures-util = "0.3.32"
glob = "0.3.3"
handlebars = "6.4.1"
hex = "0.4.3"
html5ever = "0.39.0"
indexmap = "2.14.0"
ignore = "0.4.25"
mdbook-core = { path = "crates/mdbook-core", version = "0.5.3" }
mdbook-driver = { path = "crates/mdbook-driver", version = "0.5.3" }
mdbook-html = { path = "crates/mdbook-html", version = "0.5.3" }
mdbook-markdown = { path = "crates/mdbook-markdown", version = "0.5.3" }
mdbook-preprocessor = { path = "crates/mdbook-preprocessor", version = "0.5.3" }
mdbook-renderer = { path = "crates/mdbook-renderer", version = "0.5.3" }
mdbook-summary = { path = "crates/mdbook-summary", version = "0.5.3" }
memchr = "2.8.1"
notify = "8.2.0"
notify-debouncer-mini = "0.7.0"
opener = "0.8.4"
pathdiff = "0.2.3"
pulldown-cmark = { version = "0.13.4", default-features = false, features = ["html"] } # Do not update, part of the public api.
regex = "1.12.3"
select = "0.6.1"
semver = "1.0.28"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
sha2 = "0.11.0"
shlex = "2.0.0"
snapbox = "1.2.2"
tempfile = "3.27.0"
tokio = "1.52.3"
toml = "1.1.2"
topological-sort = "0.2.2"
tower-http = "0.6.11"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
walkdir = "2.5.0"
[package] [package]
name = "mdbook" name = "mdbook"
version = "0.5.3" version = "0.4.40"
authors = [ authors = [
"Mathieu David <mathieudavid@mathieudavid.org>", "Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>", "Michael-F-Bryan <michaelfbryan@gmail.com>",
"Matt Ickstadt <mattico8@gmail.com>" "Matt Ickstadt <mattico8@gmail.com>"
] ]
documentation = "https://rust-lang.github.io/mdBook/index.html" documentation = "https://rust-lang.github.io/mdBook/index.html"
edition.workspace = true edition = "2021"
exclude = ["/guide/*"] exclude = ["/guide/*"]
keywords = ["book", "gitbook", "rustbook", "markdown"] keywords = ["book", "gitbook", "rustbook", "markdown"]
license.workspace = true license = "MPL-2.0"
readme = "README.md" readme = "README.md"
repository.workspace = true repository = "https://github.com/rust-lang/mdBook"
description = "Creates a book from markdown files" description = "Creates a book from markdown files"
rust-version.workspace = true rust-version = "1.74"
[dependencies] [dependencies]
anyhow.workspace = true anyhow = "1.0.71"
clap.workspace = true chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
clap_complete.workspace = true clap = { version = "4.3.12", features = ["cargo", "wrap_help"] }
mdbook-core.workspace = true clap_complete = "4.3.2"
mdbook-driver.workspace = true once_cell = "1.17.1"
mdbook-html.workspace = true env_logger = "0.11.1"
opener.workspace = true handlebars = "6.0"
tracing.workspace = true log = "0.4.17"
tracing-subscriber.workspace = true memchr = "2.5.0"
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.3.0"
tempfile = "3.4.0"
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
topological-sort = "0.2.2"
# Watch feature # Watch feature
ignore = { workspace = true, optional = true } notify = { version = "6.1.1", optional = true }
notify = { workspace = true, optional = true } notify-debouncer-mini = { version = "0.4.1", optional = true }
notify-debouncer-mini = { workspace = true, optional = true } ignore = { version = "0.4.20", optional = true }
pathdiff = { workspace = true, optional = true } pathdiff = { version = "0.2.1", optional = true }
walkdir = { workspace = true, optional = true } walkdir = { version = "2.3.3", optional = true }
# Serve feature # Serve feature
axum = { workspace = true, features = ["ws"], optional = true } futures-util = { version = "0.3.28", optional = true }
futures-util = { workspace = true, optional = true } tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"], optional = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"], optional = true } warp = { version = "0.3.6", default-features = false, features = ["websocket"], optional = true }
tower-http = { workspace = true, features = ["fs", "trace"], optional = true }
# Search feature
elasticlunr-rs = { version = "3.0.2", optional = true }
ammonia = { version = "4.0.0", optional = true }
[dev-dependencies] [dev-dependencies]
glob.workspace = true assert_cmd = "2.0.11"
mdbook-preprocessor.workspace = true predicates = "3.0.3"
mdbook-renderer.workspace = true select = "0.6.0"
regex.workspace = true semver = "1.0.17"
select.workspace = true pretty_assertions = "1.3.0"
semver.workspace = true walkdir = "2.3.3"
serde_json.workspace = true
snapbox = { workspace = true, features = ["diff", "dir", "term-svg", "regex", "json"] }
tempfile.workspace = true
walkdir.workspace = true
[features] [features]
default = ["watch", "serve", "search"] default = ["watch", "serve", "search"]
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"] watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"]
serve = ["dep:futures-util", "dep:tokio", "dep:axum", "dep:tower-http"] serve = ["dep:futures-util", "dep:tokio", "dep:warp"]
search = ["mdbook-html/search"] search = ["dep:elasticlunr-rs", "dep:ammonia"]
[[bin]] [[bin]]
doc = false doc = false
@ -136,19 +73,3 @@ name = "mdbook"
[[example]] [[example]]
name = "nop-preprocessor" name = "nop-preprocessor"
test = true test = true
[[example]]
name = "remove-emphasis"
path = "examples/remove-emphasis/test.rs"
crate-type = ["lib"]
test = true
[[test]]
harness = false
test = false
name = "gui"
path = "tests/gui/runner.rs"
crate-type = ["bin"]
[lints]
workspace = true

View file

@ -1,16 +1,75 @@
# mdBook # mdBook with absolute links via site-url
[![CI Status](https://github.com/rust-lang/mdBook/actions/workflows/main.yml/badge.svg)](https://github.com/rust-lang/mdBook/actions/workflows/main.yml) [![Build Status](https://github.com/rust-lang/mdBook/workflows/CI/badge.svg?event=push)](https://github.com/rust-lang/mdBook/actions?workflow=CI)
[![crates.io](https://img.shields.io/crates/v/mdbook.svg)](https://crates.io/crates/mdbook) [![crates.io](https://img.shields.io/crates/v/mdbook.svg)](https://crates.io/crates/mdbook)
[![LICENSE](https://img.shields.io/github/license/rust-lang/mdBook.svg)](LICENSE) [![LICENSE](https://img.shields.io/github/license/rust-lang/mdBook.svg)](LICENSE)
mdBook is a utility to create modern online books from Markdown files. **mdBook** is a utility to create modern online books from Markdown files.
> Code here is a [fix](#how-to-use-it) for Absolute Links with [**site-url**](https://rust-lang.github.io/mdBook/format/configuration/renderers.html?highlight=site-url#html-renderer-options) value setting in **book.toml** config.
> This [fix](https://github.com/JesusPerez/mdBook) version has been reported to [mdBook source](https://github.com/rust-lang/mdBook/) as [comment to issue 10802](https://github.com/rust-lang/mdBook/pull/1802#issuecomment-1552874669)
Check out the **[User Guide]** for a list of features and installation and usage information. Check out the **[User Guide]** for a list of features and installation and usage information.
The User Guide also serves as a demonstration to showcase what a book looks like. The User Guide also serves as a demonstration to showcase what a book looks like.
If you are interested in contributing to the development of mdBook, check out the [Contribution Guide]. If you are interested in contributing to the development of mdBook, check out the [Contribution Guide].
## How to use it
**mdBook** has to be build and optionally installed
> Try **sitefix-book** as book example using **/doc/** as **site-url**
> or use full absolute URL complete with hostnames and schemas ends with **/**
> for example: http://localhost:3000/doc/
In [sitefix-book/book.toml](sitefix-book/book.toml) add **base_url** (base href) to rendered files with **site-url** value, ends with **/**
In case relative path in **TOC** and **SUMMARY** is not fixed with **site-url** value:
- Replace **SUMMARY.md** entries to include **site-url**
- Replace **TOC** entries for each sidebar **chapter-item** to include **site-url**
<small>see example below</small>
To complement this fix:
- [mdBook Tera](https://github.com/avitex/mdbook-tera) preprocessor should be added (installed) and activated in [book.toml](sitefix-book/book.toml)
- [context.toml](sitefix-book/context.toml) for [mdBook Tera](https://github.com/avitex/mdbook-tera) preprocessor has to include **urlbase** with same **site-url** value
- Markdown **URLS** or **SRC** in book files has to start with **{{urlbase}}** prefix
```markdown
[Introduction]({{urlbase}}introduction.md)
![Image]({{urlbase}}image.jpg)
```
[my-theme/index.hbs](sitefix-book/my-theme/index.hbs) is used to set **urlbase** in **previous** and **next** navigation
You can use a script to automate builds:
- Take care of: **paths**, **book.toml**, **index.hbs**, etc.
- Use [mdBook](https://github.com/rust-lang/mdBook.git) to build rendered result to **dist** path rather than default **book**
if not installed add build target path to **mdbook** (../target/debug/)
```bash
mkdir -p /tmp/site/doc
mdbook build --dest-dir /tmp/site/doc
```
IMPORTANT: **site-url** only works with **mdbook build** with **serve** option has no sense as it is for development and **site-url** is overwritten with local path.
By using these procedures **sitefix-book** distribution build can be tested with
```bash
cd /tmp/site/doc
python3 -m http.server
```
Use a web-browser for: http://localhost:3000/doc
> For distribution or publish [mdBooks](https://github.com/rust-lang/mdBook.git) follow above instructions or use a script to set **<u>absolute_path</u>** in **book.toml** and **context.toml**
## License ## License
All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE] file. All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE] file.

View file

@ -1,38 +0,0 @@
#!/usr/bin/env bash
# This publishes the user guide to GitHub Pages.
#
# If this is a pre-release, then it goes in a separate directory called "pre-release".
# Commits are amended to avoid keeping history which can balloon the repo size.
set -ex
cargo run --no-default-features -F search -- build guide
VERSION=$(cargo metadata --format-version 1 --no-deps | jq '.packages[] | select(.name == "mdbook") | .version')
if [[ "$VERSION" == *-* ]]; then
PRERELEASE=true
else
PRERELEASE=false
fi
git fetch origin gh-pages
git worktree add gh-pages gh-pages
git config user.name "Deploy from CI"
git config user.email ""
cd gh-pages
if [[ "$PRERELEASE" == "true" ]]
then
rm -rf pre-release
mv ../guide/book pre-release
git add pre-release
git commit --amend -m "Deploy $GITHUB_SHA pre-release to gh-pages"
else
# Delete everything except pre-release and .git.
find . -mindepth 1 -maxdepth 1 -not -name "pre-release" -not -name ".git" -exec rm -rf {} +
# Copy the guide here.
find ../guide/book/ -mindepth 1 -maxdepth 1 -exec mv {} . \;
git add .
git commit --amend -m "Deploy $GITHUB_SHA to gh-pages"
fi
git push --force origin +gh-pages

View file

@ -1,44 +0,0 @@
#!/usr/bin/env bash
# Updates all compatible Cargo dependencies.
#
# I wasn't able to get Renovate to update compatible dependencies in a way
# that I like, so this script takes care of it. This uses `cargo upgrade` to
# ensure that `Cargo.toml` also gets updated. This also makes sure that all
# transitive dependencies are updated.
set -ex
git fetch origin update-dependencies
if git checkout update-dependencies
then
git reset --hard origin/master
else
git checkout -b update-dependencies
fi
cat > commit-message << 'EOF'
Update cargo dependencies
```
EOF
cargo upgrade >> commit-message
echo '```' >> commit-message
if git diff --quiet
then
echo "No changes detected, exiting."
exit 0
fi
# Also update any transitive dependencies.
cargo update
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Cargo.toml Cargo.lock
git commit -F commit-message
git push --force origin update-dependencies
gh pr create --fill \
--head update-dependencies \
--base master

View file

@ -1,12 +0,0 @@
[package]
name = "mdbook-compare"
publish = false
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
[lints]
workspace = true

View file

@ -1,26 +0,0 @@
# mdbook-compare
This is a simple utility to compare the output of two different versions of mdbook.
To use this:
1. Install [`tidy`](https://www.html-tidy.org/).
2. Install or build the initial version of mdbook that you want to compare.
3. Install or build the new version of mdbook that you want to compare.
4. Run `mdbook-compare` with the arguments to the mdbook executables and the books to build.
```sh
cargo run --manifest-path /path/to/mdBook/Cargo.toml -p mdbook-compare -- \
/path/to/orig/mdbook /path/to/my-book /path/to/new/mdbook /path/to/my-book
```
It takes two separate paths for the book to use for "before" and "after" in case you need to customize the book to run on older versions. If you don't need that, then you can use the same directory for both the before and after.
`mdbook-compare` will do the following:
1. Clean up any book directories.
2. Build the book with the first mdbook.
3. Build the book with the second mdbook.
4. The output of those two commands are stored in directories called `compare1` and `compare2`.
5. The HTML in those directories is normalized using `tidy`.
6. Runs `git diff` to compare the output.

View file

@ -1,113 +0,0 @@
//! Utility to compare the output of two different versions of mdbook.
use std::path::Path;
use std::process::Command;
macro_rules! error {
($msg:literal $($arg:tt)*) => {
eprint!("error: ");
eprintln!($msg $($arg)*);
std::process::exit(1);
};
}
fn main() {
let mut args = std::env::args().skip(1);
let (Some(mdbook1), Some(book1), Some(mdbook2), Some(book2)) =
(args.next(), args.next(), args.next(), args.next())
else {
eprintln!("error: Expected four arguments: <exe1> <dir1> <exe2> <dir2>");
std::process::exit(1);
};
let mdbook1 = Path::new(&mdbook1);
let mdbook2 = Path::new(&mdbook2);
let book1 = Path::new(&book1);
let book2 = Path::new(&book2);
let compare1 = Path::new("compare1");
let compare2 = Path::new("compare2");
clean(compare1);
clean(compare2);
clean(&book1.join("book"));
clean(&book2.join("book"));
build(mdbook1, book1);
std::fs::rename(book1.join("book"), compare1).unwrap();
build(mdbook2, book2);
std::fs::rename(book2.join("book"), compare2).unwrap();
diff(compare1, compare2);
}
fn clean(path: &Path) {
if path.exists() {
println!("removing {path:?}");
std::fs::remove_dir_all(path).unwrap();
}
}
fn build(mdbook: &Path, book: &Path) {
println!("running `{mdbook:?} build` in `{book:?}`");
let status = Command::new(mdbook)
.arg("build")
.current_dir(book)
.status()
.unwrap_or_else(|e| {
error!("expected {mdbook:?} executable to exist: {e}");
});
if !status.success() {
error!("process {mdbook:?} failed");
}
process(&book.join("book"));
}
fn process(path: &Path) {
for entry in std::fs::read_dir(path).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
process(&path);
} else {
if path.extension().is_some_and(|ext| ext == "html") {
tidy(&path);
process_html(&path);
} else {
std::fs::remove_file(path).unwrap();
}
}
}
}
fn process_html(path: &Path) {
let content = std::fs::read_to_string(path).unwrap();
let Some(start_index) = content.find("<main>") else {
return;
};
let end_index = content.rfind("</main>").unwrap();
let new_content = &content[start_index..end_index + 8];
std::fs::write(path, new_content).unwrap();
}
fn tidy(path: &Path) {
// quiet, no wrap, modify in place
let args = "-q -w 0 -m --custom-tags yes --drop-empty-elements no";
println!("running `tidy {args}` in `{path:?}`");
let status = Command::new("tidy")
.args(args.split(' '))
.arg(path)
.status()
.expect("tidy should be installed");
if !status.success() {
// Exit code 1 is a warning.
if status.code() != Some(1) {
error!("tidy failed: {status}");
}
}
}
fn diff(a: &Path, b: &Path) {
let args = "diff --no-index";
println!("running `git {args} {a:?} {b:?}`");
Command::new("git")
.args(args.split(' '))
.args([a, b])
.status()
.unwrap();
}

View file

@ -1,22 +0,0 @@
[package]
name = "mdbook-core"
version = "0.5.3"
description = "The base support library for mdbook, intended for internal use only"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
tracing.workspace = true
[dev-dependencies]
tempfile.workspace = true
[lints]
workspace = true

View file

@ -1,13 +0,0 @@
# mdbook-core
[![Documentation](https://img.shields.io/docsrs/mdbook-core)](https://docs.rs/mdbook-core)
[![crates.io](https://img.shields.io/crates/v/mdbook-core.svg)](https://crates.io/crates/mdbook-core)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the base support library for [mdBook](https://rust-lang.github.io/mdBook/). It is intended for internal use only. Other mdBook crates depend on this for any types that are shared across the crates.
> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -1,288 +0,0 @@
//! A tree structure representing a book.
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::fmt::{self, Display, Formatter};
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
#[cfg(test)]
mod tests;
/// A tree structure representing a book.
///
/// A book is just a collection of [`BookItems`] which are accessible by
/// either iterating (immutably) over the book with [`iter()`], or recursively
/// applying a closure to each item to mutate the chapters, using
/// [`for_each_mut()`].
///
/// [`iter()`]: #method.iter
/// [`for_each_mut()`]: #method.for_each_mut
#[allow(
clippy::exhaustive_structs,
reason = "This cannot be extended without breaking preprocessors."
)]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Book {
/// The items in this book.
pub items: Vec<BookItem>,
}
impl Book {
/// Create an empty book.
pub fn new() -> Self {
Default::default()
}
/// Creates a new book with the given items.
pub fn new_with_items(items: Vec<BookItem>) -> Book {
Book { items }
}
/// Get a depth-first iterator over the items in the book.
pub fn iter(&self) -> BookItems<'_> {
BookItems {
items: self.items.iter().collect(),
}
}
/// A depth-first iterator over each [`Chapter`], skipping draft chapters.
pub fn chapters(&self) -> impl Iterator<Item = &Chapter> {
self.iter().filter_map(|item| match item {
BookItem::Chapter(ch) if !ch.is_draft_chapter() => Some(ch),
_ => None,
})
}
/// Recursively apply a closure to each item in the book, allowing you to
/// mutate them.
///
/// # Note
///
/// Unlike the `iter()` method, this requires a closure instead of returning
/// an iterator. This is because using iterators can possibly allow you
/// to have iterator invalidation errors.
pub fn for_each_mut<F>(&mut self, mut func: F)
where
F: FnMut(&mut BookItem),
{
for_each_mut(&mut func, &mut self.items);
}
/// Recursively apply a closure to each non-draft chapter in the book,
/// allowing you to mutate them.
pub fn for_each_chapter_mut<F>(&mut self, mut func: F)
where
F: FnMut(&mut Chapter),
{
for_each_mut(
&mut |item| {
let BookItem::Chapter(ch) = item else {
return;
};
if ch.is_draft_chapter() {
return;
}
func(ch)
},
&mut self.items,
);
}
/// Append a `BookItem` to the `Book`.
pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
self.items.push(item.into());
self
}
}
fn for_each_mut<'a, F, I>(func: &mut F, items: I)
where
F: FnMut(&mut BookItem),
I: IntoIterator<Item = &'a mut BookItem>,
{
for item in items {
if let BookItem::Chapter(ch) = item {
for_each_mut(func, &mut ch.sub_items);
}
func(item);
}
}
/// Enum representing any type of item which can be added to a book.
#[allow(
clippy::exhaustive_enums,
reason = "This cannot be extended without breaking preprocessors."
)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BookItem {
/// A nested chapter.
Chapter(Chapter),
/// A section separator.
Separator,
/// A part title.
PartTitle(String),
}
impl From<Chapter> for BookItem {
fn from(other: Chapter) -> BookItem {
BookItem::Chapter(other)
}
}
/// The representation of a "chapter", usually mapping to a single file on
/// disk however it may contain multiple sub-chapters.
#[allow(
clippy::exhaustive_structs,
reason = "This cannot be extended without breaking preprocessors."
)]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Chapter {
/// The chapter's name.
pub name: String,
/// The chapter's contents.
pub content: String,
/// The chapter's section number, if it has one.
pub number: Option<SectionNumber>,
/// 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, or a synthetically generated
/// chapter that has no file on disk.
pub source_path: Option<PathBuf>,
/// An ordered list of the names of each chapter above this one in the hierarchy.
pub parent_names: Vec<String>,
}
impl Chapter {
/// Create a new chapter with the provided content.
pub fn new<P: Into<PathBuf>>(
name: &str,
content: String,
p: P,
parent_names: Vec<String>,
) -> Chapter {
let path: PathBuf = p.into();
Chapter {
name: name.to_string(),
content,
path: Some(path.clone()),
source_path: Some(path),
parent_names,
..Default::default()
}
}
/// Create a new draft chapter that is not attached to a source markdown file (and thus
/// has no content).
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
Chapter {
name: name.to_string(),
content: String::new(),
path: None,
source_path: None,
parent_names,
..Default::default()
}
}
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
pub fn is_draft_chapter(&self) -> bool {
self.path.is_none()
}
}
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, "{}", self.name)
}
}
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
/// a pretty `Display` impl.
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
pub struct SectionNumber(Vec<u32>);
impl SectionNumber {
/// Creates a new [`SectionNumber`].
pub fn new(numbers: impl Into<Vec<u32>>) -> SectionNumber {
SectionNumber(numbers.into())
}
}
impl Display for SectionNumber {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
write!(f, "0")
} else {
for item in &self.0 {
write!(f, "{item}.")?;
}
Ok(())
}
}
}
impl Deref for SectionNumber {
type Target = Vec<u32>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SectionNumber {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromIterator<u32> for SectionNumber {
fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
SectionNumber(it.into_iter().collect())
}
}
/// A depth-first iterator over the items in a book.
///
/// # Note
///
/// This struct shouldn't be created directly, instead prefer the
/// [`Book::iter()`] method.
pub struct BookItems<'a> {
items: VecDeque<&'a BookItem>,
}
impl<'a> Iterator for BookItems<'a> {
type Item = &'a BookItem;
fn next(&mut self) -> Option<Self::Item> {
let item = self.items.pop_front();
if let Some(BookItem::Chapter(ch)) = item {
// if we wanted a breadth-first iterator we'd `extend()` here
for sub_item in ch.sub_items.iter().rev() {
self.items.push_front(sub_item);
}
}
item
}
}

View file

@ -1,123 +0,0 @@
use super::*;
#[test]
fn section_number_has_correct_dotted_representation() {
let inputs = vec![
(vec![0], "0."),
(vec![1, 3], "1.3."),
(vec![1, 2, 3], "1.2.3."),
];
for (input, should_be) in inputs {
let section_number = SectionNumber(input).to_string();
assert_eq!(section_number, should_be);
}
}
#[test]
fn book_iter_iterates_over_sequential_items() {
let items = vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from("# Chapter 1"),
..Default::default()
}),
BookItem::Separator,
];
let book = Book::new_with_items(items);
let should_be: Vec<_> = book.items.iter().collect();
let got: Vec<_> = book.iter().collect();
assert_eq!(got, should_be);
}
#[test]
fn for_each_mut_visits_all_items() {
let items = vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from("# Chapter 1"),
number: None,
path: Some(PathBuf::from("Chapter_1/index.md")),
source_path: Some(PathBuf::from("Chapter_1/index.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
Vec::new(),
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
Vec::new(),
)),
],
}),
BookItem::Separator,
];
let mut book = Book::new_with_items(items);
let num_items = book.iter().count();
let mut visited = 0;
book.for_each_mut(|_| visited += 1);
assert_eq!(visited, num_items);
}
#[test]
fn iterate_over_nested_book_items() {
let items = vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from("# Chapter 1"),
number: None,
path: Some(PathBuf::from("Chapter_1/index.md")),
source_path: Some(PathBuf::from("Chapter_1/index.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
Vec::new(),
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
Vec::new(),
)),
],
}),
BookItem::Separator,
];
let book = Book::new_with_items(items);
let got: Vec<_> = book.iter().collect();
assert_eq!(got.len(), 5);
// checking the chapter names are in the order should be sufficient here...
let chapter_names: Vec<String> = got
.into_iter()
.filter_map(|i| match *i {
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
_ => None,
})
.collect();
let should_be: Vec<_> = vec![
String::from("Chapter 1"),
String::from("Hello World"),
String::from("Goodbye World"),
];
assert_eq!(chapter_names, should_be);
}

View file

@ -1,16 +0,0 @@
//! The base support library for mdbook, intended for internal use only.
/// The current version of `mdbook`.
///
/// This is provided as a way for custom preprocessors and renderers to do
/// compatibility checks.
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
pub mod book;
pub mod config;
pub mod utils;
/// The error types used in mdbook.
pub mod errors {
pub use anyhow::{Error, Result};
}

View file

@ -1,78 +0,0 @@
//! Utilities for dealing with HTML.
use std::borrow::Cow;
/// Escape characters to make it safe for an HTML string.
pub fn escape_html_attribute(text: &str) -> Cow<'_, str> {
let needs_escape: &[char] = &['<', '>', '\'', '"', '\\', '&'];
let mut s = text;
let mut output = String::new();
while let Some(next) = s.find(needs_escape) {
output.push_str(&s[..next]);
match s.as_bytes()[next] {
b'<' => output.push_str("&lt;"),
b'>' => output.push_str("&gt;"),
b'\'' => output.push_str("&#39;"),
b'"' => output.push_str("&quot;"),
b'\\' => output.push_str("&#92;"),
b'&' => output.push_str("&amp;"),
_ => unreachable!(),
}
s = &s[next + 1..];
}
if output.is_empty() {
Cow::Borrowed(text)
} else {
output.push_str(s);
Cow::Owned(output)
}
}
/// Escape `<`, `>`, and '&' for HTML.
pub fn escape_html(text: &str) -> Cow<'_, str> {
let needs_escape: &[char] = &['<', '>', '&'];
let mut s = text;
let mut output = String::new();
while let Some(next) = s.find(needs_escape) {
output.push_str(&s[..next]);
match s.as_bytes()[next] {
b'<' => output.push_str("&lt;"),
b'>' => output.push_str("&gt;"),
b'&' => output.push_str("&amp;"),
_ => unreachable!(),
}
s = &s[next + 1..];
}
if output.is_empty() {
Cow::Borrowed(text)
} else {
output.push_str(s);
Cow::Owned(output)
}
}
#[test]
fn attributes_are_escaped() {
assert_eq!(escape_html_attribute(""), "");
assert_eq!(escape_html_attribute("<"), "&lt;");
assert_eq!(escape_html_attribute(">"), "&gt;");
assert_eq!(escape_html_attribute("<>"), "&lt;&gt;");
assert_eq!(escape_html_attribute("<test>"), "&lt;test&gt;");
assert_eq!(escape_html_attribute("a<test>b"), "a&lt;test&gt;b");
assert_eq!(escape_html_attribute("'"), "&#39;");
assert_eq!(escape_html_attribute("\\"), "&#92;");
assert_eq!(escape_html_attribute("&"), "&amp;");
}
#[test]
fn html_is_escaped() {
assert_eq!(escape_html(""), "");
assert_eq!(escape_html("<"), "&lt;");
assert_eq!(escape_html(">"), "&gt;");
assert_eq!(escape_html("&"), "&amp;");
assert_eq!(escape_html("<>"), "&lt;&gt;");
assert_eq!(escape_html("<test>"), "&lt;test&gt;");
assert_eq!(escape_html("a<test>b"), "a&lt;test&gt;b");
assert_eq!(escape_html("'"), "'");
assert_eq!(escape_html("\\"), "\\");
}

View file

@ -1,37 +0,0 @@
//! Various helpers and utilities.
use anyhow::Error;
use std::fmt::Write;
use tracing::error;
pub mod fs;
mod html;
mod toml_ext;
pub(crate) use self::toml_ext::TomlExt;
pub use self::html::{escape_html, escape_html_attribute};
/// Defines a `static` with a [`regex::Regex`].
#[macro_export]
macro_rules! static_regex {
($name:ident, $regex:literal) => {
static $name: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new($regex).unwrap());
};
($name:ident, bytes, $regex:literal) => {
static $name: std::sync::LazyLock<regex::bytes::Regex> =
std::sync::LazyLock::new(|| regex::bytes::Regex::new($regex).unwrap());
};
}
/// Prints a "backtrace" of some `Error`.
pub fn log_backtrace(e: &Error) {
let mut message = format!("{e}");
for cause in e.chain().skip(1) {
write!(message, "\n\tCaused by: {cause}").unwrap();
}
error!("{message}");
}

View file

@ -1,32 +0,0 @@
[package]
name = "mdbook-driver"
version = "0.5.3"
description = "High-level library for running mdBook"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
indexmap.workspace = true
mdbook-core.workspace = true
mdbook-html.workspace = true
mdbook-markdown.workspace = true
mdbook-preprocessor.workspace = true
mdbook-renderer.workspace = true
mdbook-summary.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
shlex.workspace = true
tempfile.workspace = true
toml.workspace = true
topological-sort.workspace = true
tracing.workspace = true
[lints]
workspace = true
[features]
search = ["mdbook-html/search"]

View file

@ -1,13 +0,0 @@
# mdbook-driver
[![Documentation](https://img.shields.io/docsrs/mdbook-driver)](https://docs.rs/mdbook-driver)
[![crates.io](https://img.shields.io/crates/v/mdbook-driver.svg)](https://crates.io/crates/mdbook-driver)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the high-level Rust library for running [mdBook](https://rust-lang.github.io/mdBook/). New books can be created using [`BookBuilder`](https://docs.rs/mdbook-driver/latest/mdbook_driver/init/struct.BookBuilder.html). The primary type [`MDBook`](https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html) can be used to manage and render books.
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -1,9 +0,0 @@
//! Built-in preprocessors.
pub use self::cmd::CmdPreprocessor;
pub use self::index::IndexPreprocessor;
pub use self::links::LinkPreprocessor;
mod cmd;
mod index;
mod links;

View file

@ -1,88 +0,0 @@
//! Built-in renderers.
//!
//! The HTML renderer can be found in the [`mdbook_html`] crate.
use anyhow::{Context, Result, bail};
use mdbook_core::utils::fs;
use mdbook_renderer::{RenderContext, Renderer};
use std::process::Stdio;
use tracing::{error, info, trace, warn};
pub use self::markdown_renderer::MarkdownRenderer;
mod markdown_renderer;
/// A generic renderer which will shell out to an arbitrary executable.
///
/// See <https://rust-lang.github.io/mdBook/for_developers/backends.html>
/// for a description of the renderer protocol.
#[derive(Debug, Clone, PartialEq)]
pub struct CmdRenderer {
name: String,
cmd: String,
}
impl CmdRenderer {
/// Create a new `CmdRenderer` which will invoke the provided `cmd` string.
pub fn new(name: String, cmd: String) -> CmdRenderer {
CmdRenderer { name, cmd }
}
}
impl Renderer for CmdRenderer {
fn name(&self) -> &str {
&self.name
}
fn render(&self, ctx: &RenderContext) -> Result<()> {
info!("Invoking the \"{}\" renderer", self.name);
let optional_key = format!("output.{}.optional", self.name);
let optional = match ctx.config.get(&optional_key) {
Ok(Some(value)) => value,
Err(e) => bail!("expected bool for `{optional_key}`: {e}"),
Ok(None) => false,
};
let _ = fs::create_dir_all(&ctx.destination);
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
let mut child = match cmd
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.current_dir(&ctx.destination)
.spawn()
{
Ok(c) => c,
Err(e) => {
return crate::handle_command_error(
e, optional, "output", "backend", &self.name, &self.cmd,
);
}
};
let mut stdin = child.stdin.take().expect("Child has stdin");
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
// Looks like the backend hung up before we could finish
// sending it the render context. Log the error and keep going
warn!("Error writing the RenderContext to the backend, {}", e);
}
// explicitly close the `stdin` file handle
drop(stdin);
let status = child
.wait()
.with_context(|| "Error waiting for the backend to complete")?;
trace!("{} exited with output: {:?}", self.cmd, status);
if !status.success() {
error!("Renderer exited with non-zero return code.");
bail!("The \"{}\" renderer failed", self.name);
} else {
Ok(())
}
}
}

View file

@ -1,131 +0,0 @@
//! High-level library for running mdBook.
//!
//! This is the high-level library for running
//! [mdBook](https://rust-lang.github.io/mdBook/). There are several
//! reasons for using the programmatic API (over the CLI):
//!
//! - Integrate mdBook in a current project.
//! - Extend the capabilities of mdBook.
//! - Do some processing or test before building your book.
//! - Accessing the public API to help create a new Renderer.
//!
//! ## Additional crates
//!
//! In addition to `mdbook-driver`, there are several other crates available
//! for using and extending mdBook:
//!
//! - [`mdbook_preprocessor`]: Provides support for implementing preprocessors.
//! - [`mdbook_renderer`]: Provides support for implementing renderers.
//! - [`mdbook_markdown`]: The Markdown renderer.
//! - [`mdbook_summary`]: The `SUMMARY.md` parser.
//! - [`mdbook_html`]: The HTML renderer.
//! - [`mdbook_core`]: An internal library that is used by the other crates
//! for shared types. Types from this crate are rexported from the other
//! crates as appropriate.
//!
//! ## Cargo features
//!
//! The following cargo features are available:
//!
//! - `search`: Enables the search index in the HTML renderer.
//!
//! ## Examples
//!
//! If creating a new book from scratch, you'll want to get a [`init::BookBuilder`] via
//! the [`MDBook::init()`] method.
//!
//! ```rust,no_run
//! use mdbook_driver::MDBook;
//! use mdbook_driver::config::Config;
//!
//! let root_dir = "/path/to/book/root";
//!
//! // create a default config and change a couple things
//! let mut cfg = Config::default();
//! cfg.book.title = Some("My Book".to_string());
//! cfg.book.authors.push("Michael-F-Bryan".to_string());
//!
//! MDBook::init(root_dir)
//! .create_gitignore(true)
//! .with_config(cfg)
//! .build()
//! .expect("Book generation failed");
//! ```
//!
//! You can also load an existing book and build it.
//!
//! ```rust,no_run
//! use mdbook_driver::MDBook;
//!
//! let root_dir = "/path/to/book/root";
//!
//! let mut md = MDBook::load(root_dir)
//! .expect("Unable to load the book");
//! md.build().expect("Building failed");
//! ```
pub mod builtin_preprocessors;
pub mod builtin_renderers;
pub mod init;
mod load;
mod mdbook;
use anyhow::{Context, Result, bail};
pub use mdbook::MDBook;
pub use mdbook_core::{book, config, errors};
use shlex::Shlex;
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::{error, warn};
/// Creates a [`Command`] for command renderers and preprocessors.
fn compose_command(cmd: &str, root: &Path) -> Result<Command> {
let mut words = Shlex::new(cmd);
let exe = match words.next() {
Some(e) => PathBuf::from(e),
None => bail!("Command string was empty"),
};
let exe = if exe.components().count() == 1 {
// Search PATH for the executable.
exe
} else {
// Relative path is relative to book root.
root.join(&exe)
};
let mut cmd = Command::new(exe);
for arg in words {
cmd.arg(arg);
}
Ok(cmd)
}
/// Handles a failure for a preprocessor or renderer.
fn handle_command_error(
error: std::io::Error,
optional: bool,
key: &str,
what: &str,
name: &str,
cmd: &str,
) -> Result<()> {
if let std::io::ErrorKind::NotFound = error.kind() {
if optional {
warn!(
"The command `{cmd}` for {what} `{name}` was not found, \
but is marked as optional.",
);
return Ok(());
} else {
error!(
"The command `{cmd}` wasn't found, is the `{name}` {what} installed? \
If you want to ignore this error when the `{name}` {what} is not installed, \
set `optional = true` in the `[{key}.{name}]` section of the book.toml configuration file.",
);
}
}
Err(error).with_context(|| format!("Unable to run the {what} `{name}`"))?
}

View file

@ -1,309 +0,0 @@
use anyhow::{Context, Result};
use mdbook_core::book::{Book, BookItem, Chapter};
use mdbook_core::config::BuildConfig;
use mdbook_core::utils::{escape_html, fs};
use mdbook_summary::{Link, Summary, SummaryItem, parse_summary};
use std::path::Path;
use tracing::debug;
/// Load a book into memory from its `src/` directory.
pub(crate) fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
let src_dir = src_dir.as_ref();
let summary_md = src_dir.join("SUMMARY.md");
let summary_content = fs::read_to_string(&summary_md)?;
let summary = parse_summary(&summary_content)
.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")?;
}
load_book_from_disk(&summary, src_dir)
}
fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
let mut items: Vec<_> = summary
.prefix_chapters
.iter()
.chain(summary.numbered_chapters.iter())
.chain(summary.suffix_chapters.iter())
.collect();
while let Some(next) = items.pop() {
if let SummaryItem::Link(ref link) = *next {
if let Some(ref location) = link.location {
let filename = src_dir.join(location);
if !filename.exists() {
if let Some(parent) = filename.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
debug!("Creating missing file {}", filename.display());
let title = escape_html(&link.name);
fs::write(&filename, format!("# {title}\n"))?;
}
}
items.extend(&link.nested_items);
}
}
Ok(())
}
/// Use the provided `Summary` to load a `Book` from disk.
///
/// You need to pass in the book's source directory because all the links in
/// `SUMMARY.md` give the chapter locations relative to it.
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
debug!("Loading the book from disk");
let src_dir = src_dir.as_ref();
let prefix = summary.prefix_chapters.iter();
let numbered = summary.numbered_chapters.iter();
let suffix = summary.suffix_chapters.iter();
let summary_items = prefix.chain(numbered).chain(suffix);
let mut chapters = Vec::new();
for summary_item in summary_items {
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
chapters.push(chapter);
}
Ok(Book::new_with_items(chapters))
}
fn load_summary_item<P: AsRef<Path> + Clone>(
item: &SummaryItem,
src_dir: P,
parent_names: Vec<String>,
) -> Result<BookItem> {
match item {
SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(link) => load_chapter(link, src_dir, parent_names).map(BookItem::Chapter),
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
_ => panic!("SummaryItem {item:?} not covered"),
}
}
fn load_chapter<P: AsRef<Path>>(
link: &Link,
src_dir: P,
parent_names: Vec<String>,
) -> Result<Chapter> {
let src_dir = src_dir.as_ref();
let mut ch = if let Some(ref link_location) = link.location {
debug!("Loading {} ({})", link.name, link_location.display());
let location = if link_location.is_absolute() {
link_location.clone()
} else {
src_dir.join(link_location)
};
let mut content = std::fs::read_to_string(&location)
.with_context(|| format!("failed to read chapter `{}`", link_location.display()))?;
if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
content.replace_range(..3, "");
}
let stripped = location
.strip_prefix(src_dir)
.expect("Chapters are always inside a book");
Chapter::new(&link.name, content, stripped, parent_names.clone())
} else {
Chapter::new_draft(&link.name, parent_names.clone())
};
let mut sub_item_parents = parent_names;
ch.number = link.number.clone();
sub_item_parents.push(link.name.clone());
let sub_items = link
.nested_items
.iter()
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
.collect::<Result<Vec<_>>>()?;
ch.sub_items = sub_items;
Ok(ch)
}
#[cfg(test)]
mod tests {
use super::*;
use mdbook_core::book::SectionNumber;
use std::path::PathBuf;
use tempfile::{Builder as TempFileBuilder, TempDir};
const DUMMY_SRC: &str = "
# Dummy Chapter
this is some dummy text.
And here is some \
more text.
";
/// Create a dummy `Link` in a temporary directory.
fn dummy_link() -> (Link, TempDir) {
let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let chapter_path = temp.path().join("chapter_1.md");
fs::write(&chapter_path, DUMMY_SRC).unwrap();
let link = Link::new("Chapter 1", chapter_path);
(link, temp)
}
/// Create a nested `Link` written to a temporary directory.
fn nested_links() -> (Link, TempDir) {
let (mut root, temp_dir) = dummy_link();
let second_path = temp_dir.path().join("second.md");
fs::write(&second_path, "Hello World!").unwrap();
let mut second = Link::new("Nested Chapter 1", &second_path);
second.number = Some(SectionNumber::new([1, 2]));
root.nested_items.push(second.clone().into());
root.nested_items.push(SummaryItem::Separator);
root.nested_items.push(second.into());
(root, temp_dir)
}
#[test]
fn load_a_single_chapter_from_disk() {
let (link, temp_dir) = dummy_link();
let should_be = Chapter::new(
"Chapter 1",
DUMMY_SRC.to_string(),
"chapter_1.md",
Vec::new(),
);
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn load_a_single_chapter_with_utf8_bom_from_disk() {
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let chapter_path = temp_dir.path().join("chapter_1.md");
fs::write(&chapter_path, format!("\u{feff}{DUMMY_SRC}")).unwrap();
let link = Link::new("Chapter 1", chapter_path);
let should_be = Chapter::new(
"Chapter 1",
DUMMY_SRC.to_string(),
"chapter_1.md",
Vec::new(),
);
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn cant_load_a_nonexistent_chapter() {
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
let got = load_chapter(&link, "", Vec::new());
assert!(got.is_err());
}
#[test]
fn load_recursive_link_with_separators() {
let (root, temp) = nested_links();
let mut nested = Chapter::new(
"Nested Chapter 1",
String::from("Hello World!"),
"second.md",
vec![String::from("Chapter 1")],
);
nested.number = Some(SectionNumber::new([1, 2]));
let mut chapter =
Chapter::new("Chapter 1", String::from(DUMMY_SRC), "chapter_1.md", vec![]);
chapter.sub_items = vec![
BookItem::Chapter(nested.clone()),
BookItem::Separator,
BookItem::Chapter(nested),
];
let should_be = BookItem::Chapter(chapter);
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn load_a_book_with_a_single_chapter() {
let (link, temp) = dummy_link();
let mut summary = Summary::default();
summary.numbered_chapters = vec![SummaryItem::Link(link)];
let chapter = Chapter::new(
"Chapter 1",
String::from(DUMMY_SRC),
PathBuf::from("chapter_1.md"),
vec![],
);
let items = vec![BookItem::Chapter(chapter)];
let should_be = Book::new_with_items(items);
let got = load_book_from_disk(&summary, temp.path()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn cant_load_chapters_with_an_empty_path() {
let (_, temp) = dummy_link();
let mut summary = Summary::default();
let link = Link::new("Empty", "");
summary.numbered_chapters = vec![SummaryItem::Link(link)];
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());
}
#[test]
fn cant_load_chapters_when_the_link_is_a_directory() {
let (_, temp) = dummy_link();
let dir = temp.path().join("nested");
fs::create_dir_all(&dir).unwrap();
let mut summary = Summary::default();
let link = Link::new("nested", dir);
summary.numbered_chapters = vec![SummaryItem::Link(link)];
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());
}
#[test]
fn cant_open_summary_md() {
let cfg = BuildConfig::default();
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let got = load_book(&temp_dir, &cfg);
assert!(got.is_err());
let error_message = got.err().unwrap().to_string();
let expected = format!(
r#"failed to read `{}`"#,
temp_dir.path().join("SUMMARY.md").display()
);
assert_eq!(error_message, expected);
}
}

View file

@ -1,569 +0,0 @@
//! The high-level interface for loading and rendering books.
use crate::builtin_preprocessors::{CmdPreprocessor, IndexPreprocessor, LinkPreprocessor};
use crate::builtin_renderers::{CmdRenderer, MarkdownRenderer};
use crate::init::BookBuilder;
use crate::load::{load_book, load_book_from_disk};
use anyhow::{Context, Error, Result, bail};
use indexmap::IndexMap;
use mdbook_core::book::{Book, BookItem, BookItems};
use mdbook_core::config::{Config, RustEdition};
use mdbook_core::utils::fs;
use mdbook_html::HtmlHandlebars;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use mdbook_renderer::{RenderContext, Renderer};
use mdbook_summary::Summary;
use serde::Deserialize;
use std::ffi::OsString;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::Builder as TempFileBuilder;
use topological_sort::TopologicalSort;
use tracing::{debug, info, trace, warn};
#[cfg(test)]
mod tests;
/// The object used to manage and build a book.
pub struct MDBook {
/// The book's root directory.
pub root: PathBuf,
/// The configuration used to tweak now a book is built.
pub config: Config,
/// A representation of the book's contents in memory.
pub book: Book,
/// Renderers to execute.
renderers: IndexMap<String, Box<dyn Renderer>>,
/// Pre-processors to be run on the book.
preprocessors: IndexMap<String, Box<dyn Preprocessor>>,
}
impl MDBook {
/// Load a book from its root directory on disk.
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
let book_root = book_root.into();
let config_location = book_root.join("book.toml");
let mut config = if config_location.exists() {
debug!("Loading config from {}", config_location.display());
Config::from_disk(&config_location)?
} else {
Config::default()
};
config.update_from_env()?;
if tracing::enabled!(tracing::Level::TRACE) {
for line in format!("Config: {config:#?}").lines() {
trace!("{}", line);
}
}
MDBook::load_with_config(book_root, config)
}
/// Load a book from its root directory using a custom `Config`.
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
let root = book_root.into();
let src_dir = root.join(&config.book.src);
let book = load_book(src_dir, &config.build)?;
let renderers = determine_renderers(&config)?;
let preprocessors = determine_preprocessors(&config, &root)?;
Ok(MDBook {
root,
config,
book,
renderers,
preprocessors,
})
}
/// Load a book from its root directory using a custom `Config` and a custom summary.
pub fn load_with_config_and_summary<P: Into<PathBuf>>(
book_root: P,
config: Config,
summary: Summary,
) -> Result<MDBook> {
let root = book_root.into();
let src_dir = root.join(&config.book.src);
let book = load_book_from_disk(&summary, src_dir)?;
let renderers = determine_renderers(&config)?;
let preprocessors = determine_preprocessors(&config, &root)?;
Ok(MDBook {
root,
config,
book,
renderers,
preprocessors,
})
}
/// Returns a flat depth-first iterator over the [`BookItem`]s of the book.
///
/// ```no_run
/// # use mdbook_driver::MDBook;
/// # use mdbook_driver::book::BookItem;
/// # let book = MDBook::load("mybook").unwrap();
/// for item in book.iter() {
/// match *item {
/// BookItem::Chapter(ref chapter) => {},
/// BookItem::Separator => {},
/// BookItem::PartTitle(ref title) => {}
/// _ => {}
/// }
/// }
///
/// // would print something like this:
/// // 1. Chapter 1
/// // 1.1 Sub Chapter
/// // 1.2 Sub Chapter
/// // 2. Chapter 2
/// //
/// // etc.
/// ```
pub fn iter(&self) -> BookItems<'_> {
self.book.iter()
}
/// `init()` gives you a `BookBuilder` which you can use to setup a new book
/// and its accompanying directory structure.
///
/// The `BookBuilder` creates some boilerplate files and directories to get
/// you started with your book.
///
/// ```text
/// book-test/
/// ├── book
/// └── src
/// ├── chapter_1.md
/// └── SUMMARY.md
/// ```
///
/// It uses the path provided as the root directory for your book, then adds
/// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
/// to get you started.
pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
BookBuilder::new(book_root)
}
/// Tells the renderer to build our book and put it in the build directory.
pub fn build(&self) -> Result<()> {
info!("Book building has started");
for renderer in self.renderers.values() {
self.execute_build_process(&**renderer)?;
}
Ok(())
}
/// Run preprocessors and return the final book.
pub fn preprocess_book(&self, renderer: &dyn Renderer) -> Result<(Book, PreprocessorContext)> {
let preprocess_ctx = PreprocessorContext::new(
self.root.clone(),
self.config.clone(),
renderer.name().to_string(),
);
let mut preprocessed_book = self.book.clone();
for preprocessor in self.preprocessors.values() {
if preprocessor_should_run(&**preprocessor, renderer, &self.config)? {
debug!("Running the {} preprocessor.", preprocessor.name());
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
}
}
Ok((preprocessed_book, preprocess_ctx))
}
/// Run the entire build process for a particular [`Renderer`].
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
let (preprocessed_book, preprocess_ctx) = self.preprocess_book(renderer)?;
let name = renderer.name();
let build_dir = self.build_dir_for(name);
let mut render_context = RenderContext::new(
self.root.clone(),
preprocessed_book,
self.config.clone(),
build_dir,
);
render_context
.chapter_titles
.extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
info!("Running the {} backend", renderer.name());
renderer
.render(&render_context)
.with_context(|| "Rendering failed")
}
/// You can change the default renderer to another one by using this method.
/// The only requirement is that your renderer implement the [`Renderer`]
/// trait.
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
self.renderers
.insert(renderer.name().to_string(), Box::new(renderer));
self
}
/// Register a [`Preprocessor`] to be used when rendering the book.
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
self.preprocessors
.insert(preprocessor.name().to_string(), Box::new(preprocessor));
self
}
/// Run `rustdoc` tests on the book, linking against the provided libraries.
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
// test_chapter with chapter:None will run all tests.
self.test_chapter(library_paths, None)
}
/// 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 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()?;
let mut chapter_found = false;
struct TestRenderer;
impl Renderer for TestRenderer {
// FIXME: Is "test" the proper renderer name to use here?
fn name(&self) -> &str {
"test"
}
fn render(&self, _: &RenderContext) -> Result<()> {
Ok(())
}
}
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 {
let chapter_path = match ch.path {
Some(ref path) if !path.as_os_str().is_empty() => path,
_ => continue,
};
if let Some(chapter) = chapter {
if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
if chapter == "?" {
info!("Skipping chapter '{}'...", ch.name);
}
continue;
}
}
chapter_found = true;
info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
// write preprocessed file to tempdir
let path = temp_dir.path().join(chapter_path);
fs::write(&path, &ch.content)?;
let mut cmd = Command::new("rustdoc");
cmd.current_dir(temp_dir.path())
.arg(chapter_path)
.arg("--test")
.args(&library_args);
if let Some(edition) = self.config.rust.edition {
match edition {
RustEdition::E2015 => {
cmd.args(["--edition", "2015"]);
}
RustEdition::E2018 => {
cmd.args(["--edition", "2018"]);
}
RustEdition::E2021 => {
cmd.args(["--edition", "2021"]);
}
RustEdition::E2024 => {
cmd.args(["--edition", "2024"]);
}
_ => panic!("RustEdition {edition:?} not covered"),
}
}
if color_output {
cmd.args(["--color", "always"]);
}
debug!("running {:?}", cmd);
let output = cmd
.output()
.with_context(|| "failed to execute `rustdoc`")?;
if !output.status.success() {
failed = true;
eprintln!(
"ERROR rustdoc returned an error:\n\
\n--- stdout\n{}\n--- stderr\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
}
}
if failed {
bail!("One or more tests failed");
}
if let Some(chapter) = chapter {
if !chapter_found {
bail!("Chapter not found: {}", chapter);
}
}
Ok(())
}
/// The logic for determining where a backend should put its build
/// artefacts.
///
/// If there is only 1 renderer, put it in the directory pointed to by the
/// `build.build_dir` key in [`Config`]. If there is more than one then the
/// renderer gets its own directory within the main build dir.
///
/// i.e. If there were only one renderer (in this case, the HTML renderer):
///
/// - build/
/// - index.html
/// - ...
///
/// Otherwise if there are multiple:
///
/// - build/
/// - epub/
/// - my_awesome_book.epub
/// - html/
/// - index.html
/// - ...
/// - latex/
/// - my_awesome_book.tex
///
pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
let build_dir = self.root.join(&self.config.build.build_dir);
if self.renderers.len() <= 1 {
build_dir
} else {
build_dir.join(backend_name)
}
}
/// Get the directory containing this book's source files.
pub fn source_dir(&self) -> PathBuf {
self.root.join(&self.config.book.src)
}
/// Get the directory containing the theme resources for the book.
pub fn theme_dir(&self) -> PathBuf {
self.config
.html_config()
.unwrap_or_default()
.theme_dir(&self.root)
}
}
/// An `output` table.
#[derive(Deserialize)]
struct OutputConfig {
command: Option<String>,
}
/// Look at the `Config` and try to figure out what renderers to use.
fn determine_renderers(config: &Config) -> Result<IndexMap<String, Box<dyn Renderer>>> {
let mut renderers = IndexMap::new();
let outputs = config.outputs::<OutputConfig>()?;
renderers.extend(outputs.into_iter().map(|(key, table)| {
let renderer = if key == "html" {
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
} else if key == "markdown" {
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
} else {
let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
Box::new(CmdRenderer::new(key.clone(), command))
};
(key, renderer)
}));
// if we couldn't find anything, add the HTML renderer as a default
if renderers.is_empty() {
renderers.insert("html".to_string(), Box::new(HtmlHandlebars::new()));
}
Ok(renderers)
}
const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
let name = pre.name();
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
}
/// A `preprocessor` table.
#[derive(Deserialize)]
struct PreprocessorConfig {
command: Option<String>,
#[serde(default)]
before: Vec<String>,
#[serde(default)]
after: Vec<String>,
#[serde(default)]
optional: bool,
}
/// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(
config: &Config,
root: &Path,
) -> Result<IndexMap<String, Box<dyn Preprocessor>>> {
// Collect the names of all preprocessors intended to be run, and the order
// in which they should be run.
let mut preprocessor_names = TopologicalSort::<String>::new();
if config.build.use_default_preprocessors {
for name in DEFAULT_PREPROCESSORS {
preprocessor_names.insert(name.to_string());
}
}
let preprocessor_table = config.preprocessors::<PreprocessorConfig>()?;
for (name, table) in preprocessor_table.iter() {
preprocessor_names.insert(name.to_string());
let exists = |name| {
(config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
|| preprocessor_table.contains_key(name)
};
for after in &table.before {
if !exists(&after) {
// Only warn so that preprocessors can be toggled on and off (e.g. for
// troubleshooting) without having to worry about order too much.
warn!(
"preprocessor.{}.after contains \"{}\", which was not found",
name, after
);
} else {
preprocessor_names.add_dependency(name, after);
}
}
for before in &table.after {
if !exists(&before) {
// See equivalent warning above for rationale
warn!(
"preprocessor.{}.before contains \"{}\", which was not found",
name, before
);
} else {
preprocessor_names.add_dependency(before, name);
}
}
}
// Now that all links have been established, queue preprocessors in a suitable order
let mut preprocessors = IndexMap::with_capacity(preprocessor_names.len());
// `pop_all()` returns an empty vector when no more items are not being depended upon
for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
.take_while(|names| !names.is_empty())
{
// The `topological_sort` crate does not guarantee a stable order for ties, even across
// runs of the same program. Thus, we break ties manually by sorting.
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
// values ([1]), which may not be an alphabetical sort.
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
// preprocessor execution order.
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
names.sort();
for name in names {
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
"links" => Box::new(LinkPreprocessor::new()),
"index" => Box::new(IndexPreprocessor::new()),
_ => {
// The only way to request a custom preprocessor is through the `preprocessor`
// table, so it must exist, be a table, and contain the key.
let table = &preprocessor_table[&name];
let command = table
.command
.to_owned()
.unwrap_or_else(|| format!("mdbook-{name}"));
Box::new(CmdPreprocessor::new(
name.clone(),
command,
root.to_owned(),
table.optional,
))
}
};
preprocessors.insert(name, preprocessor);
}
}
// "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
// Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
if preprocessor_names.is_empty() {
Ok(preprocessors)
} else {
Err(Error::msg("Cyclic dependency detected in preprocessors"))
}
}
/// Check whether we should run a particular `Preprocessor` in combination
/// with the renderer, falling back to `Preprocessor::supports_renderer()`
/// method if the user doesn't say anything.
///
/// The `build.use-default-preprocessors` config option can be used to ensure
/// default preprocessors always run if they support the renderer.
fn preprocessor_should_run(
preprocessor: &dyn Preprocessor,
renderer: &dyn Renderer,
cfg: &Config,
) -> Result<bool> {
// default preprocessors should be run by default (if supported)
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
return preprocessor.supports_renderer(renderer.name());
}
let key = format!("preprocessor.{}.renderers", preprocessor.name());
let renderer_name = renderer.name();
match cfg.get::<Vec<String>>(&key) {
Ok(Some(explicit_renderers)) => {
Ok(explicit_renderers.iter().any(|name| name == renderer_name))
}
Ok(None) => preprocessor.supports_renderer(renderer_name),
Err(e) => bail!("failed to get `{key}`: {e}"),
}
}

View file

@ -1,284 +0,0 @@
use super::*;
use std::str::FromStr;
use toml::value::{Table, Value};
#[test]
fn config_defaults_to_html_renderer_if_empty() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
assert!(cfg.outputs::<toml::Value>().unwrap().is_empty());
let got = determine_renderers(&cfg).unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "html");
}
#[test]
fn add_a_random_renderer_to_the_config() {
let mut cfg = Config::default();
cfg.set("output.random", Table::new()).unwrap();
let got = determine_renderers(&cfg).unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random");
}
#[test]
fn add_a_random_renderer_with_custom_command_to_the_config() {
let mut cfg = Config::default();
let mut table = Table::new();
table.insert("command".to_string(), Value::String("false".to_string()));
cfg.set("output.random", table).unwrap();
let got = determine_renderers(&cfg).unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random");
}
#[test]
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
let cfg = Config::default();
// make sure we haven't got anything in the `preprocessor` table
assert!(cfg.preprocessors::<toml::Value>().unwrap().is_empty());
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
let names: Vec<_> = got.values().map(|p| p.name()).collect();
assert_eq!(names, ["index", "links"]);
}
#[test]
fn use_default_preprocessors_works() {
let mut cfg = Config::default();
cfg.build.use_default_preprocessors = false;
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
assert_eq!(got.len(), 0);
}
#[test]
fn can_determine_third_party_preprocessors() {
let cfg_str = r#"
[book]
title = "Some Book"
[preprocessor.random]
[build]
build-dir = "outputs"
create-missing = false
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure the `preprocessor.random` table exists
assert!(cfg.get::<Value>("preprocessor.random").unwrap().is_some());
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
assert!(got.contains_key("random"));
}
#[test]
fn preprocessors_can_provide_their_own_commands() {
let cfg_str = r#"
[preprocessor.random]
command = "python random.py"
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure the `preprocessor.random` table exists
let random = cfg
.get::<OutputConfig>("preprocessor.random")
.unwrap()
.unwrap();
assert_eq!(random.command, Some("python random.py".to_string()));
}
#[test]
fn preprocessor_before_must_be_array() {
let cfg_str = r#"
[preprocessor.random]
before = 0
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
}
#[test]
fn preprocessor_after_must_be_array() {
let cfg_str = r#"
[preprocessor.random]
after = 0
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
}
#[test]
fn preprocessor_order_is_honored() {
let cfg_str = r#"
[preprocessor.random]
before = [ "last" ]
after = [ "index" ]
[preprocessor.last]
after = [ "links", "index" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
let index = |name| preprocessors.get_index_of(name).unwrap();
let assert_before = |before, after| {
if index(before) >= index(after) {
eprintln!("Preprocessor order:");
for preprocessor in preprocessors.keys() {
eprintln!(" {}", preprocessor);
}
panic!("{before} should come before {after}");
}
};
assert_before("index", "random");
assert_before("index", "last");
assert_before("random", "last");
assert_before("links", "last");
}
#[test]
fn cyclic_dependencies_are_detected() {
let cfg_str = r#"
[preprocessor.links]
before = [ "index" ]
[preprocessor.index]
before = [ "links" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
}
#[test]
fn dependencies_dont_register_undefined_preprocessors() {
let cfg_str = r#"
[preprocessor.links]
before = [ "random" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
// Does not contain "random"
assert_eq!(preprocessors.keys().collect::<Vec<_>>(), ["index", "links"]);
}
#[test]
fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
let cfg_str = r#"
[preprocessor.random]
before = [ "links" ]
[build]
use-default-preprocessors = false
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
// Does not contain "links"
assert_eq!(preprocessors.keys().collect::<Vec<_>>(), ["random"]);
}
#[test]
fn config_respects_preprocessor_selection() {
let cfg_str = r#"
[preprocessor.links]
renderers = ["html"]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let html_renderer = HtmlHandlebars::default();
let pre = LinkPreprocessor::new();
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg).unwrap();
assert!(should_run);
}
struct BoolPreprocessor(bool);
impl Preprocessor for BoolPreprocessor {
fn name(&self) -> &str {
"bool-preprocessor"
}
fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
unimplemented!()
}
fn supports_renderer(&self, _renderer: &str) -> Result<bool> {
Ok(self.0)
}
}
#[test]
fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
let cfg = Config::default();
let html = HtmlHandlebars::new();
let should_be = true;
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg).unwrap();
assert_eq!(got, should_be);
let should_be = false;
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg).unwrap();
assert_eq!(got, should_be);
}
// Default is to sort preprocessors alphabetically.
#[test]
fn preprocessor_sorted_by_name() {
let cfg_str = r#"
[preprocessor.xyz]
[preprocessor.abc]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
let names: Vec<_> = got.values().map(|p| p.name()).collect();
assert_eq!(names, ["abc", "index", "links", "xyz"]);
}
// Default is to sort renderers alphabetically.
#[test]
fn renderers_sorted_by_name() {
let cfg_str = r#"
[output.xyz]
[output.abc]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let got = determine_renderers(&cfg).unwrap();
let names: Vec<_> = got.values().map(|p| p.name()).collect();
assert_eq!(names, ["abc", "xyz"]);
}

View file

@ -1,37 +0,0 @@
[package]
name = "mdbook-html"
version = "0.5.3"
description = "mdBook HTML renderer"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
ego-tree.workspace = true
elasticlunr-rs = { workspace = true, optional = true }
font-awesome-as-a-crate.workspace = true
handlebars.workspace = true
hex.workspace = true
html5ever.workspace = true
indexmap.workspace = true
mdbook-core.workspace = true
mdbook-markdown.workspace = true
mdbook-renderer.workspace = true
pulldown-cmark.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
tracing.workspace = true
[dev-dependencies]
tempfile.workspace = true
toml.workspace = true
[lints]
workspace = true
[features]
search = ["dep:elasticlunr-rs"]

View file

@ -1,13 +0,0 @@
# mdbook-html
[![Documentation](https://img.shields.io/docsrs/mdbook-html)](https://docs.rs/mdbook-html)
[![crates.io](https://img.shields.io/crates/v/mdbook-html.svg)](https://crates.io/crates/mdbook-html)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the HTML renderer for [mdBook](https://rust-lang.github.io/mdBook/). This is intended for internal use only. It is automatically included by [`mdbook-driver`](https://crates.io/crates/mdbook-driver) to render books to HTML.
> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -1,408 +0,0 @@
/* Base styles and content styles */
:root {
/* Browser default font-size is 16px, this way 1 rem = 10px */
font-size: 62.5%;
color-scheme: var(--color-scheme);
}
html {
font-family: "Open Sans", sans-serif;
color: var(--fg);
background-color: var(--bg);
text-size-adjust: none;
-webkit-text-size-adjust: none;
}
body {
margin: 0;
font-size: 1.6rem;
overflow-x: hidden;
}
code {
font-family: var(--mono-font) !important;
font-size: var(--code-font-size);
direction: ltr !important;
}
/* make long words/inline code not x overflow */
main {
overflow-wrap: break-word;
}
/* make wide tables scroll if they overflow */
.table-wrapper {
overflow-x: auto;
}
/* Don't change font size in headers. */
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
font-size: unset;
}
.left { float: left; }
.right { float: right; }
.boring { opacity: 0.6; }
.hide-boring .boring { display: none; }
.hidden { display: none !important; }
h2, h3 { margin-block-start: 2.5em; }
h4, h5 { margin-block-start: 2em; }
.header + .header h3,
.header + .header h4,
.header + .header h5 {
margin-block-start: 1em;
}
h1:target::before,
h2:target::before,
h3:target::before,
h4:target::before,
h5:target::before,
h6:target::before,
dt:target::before {
display: inline-block;
content: "»";
margin-inline-start: -30px;
width: 30px;
}
/* This is broken on Safari as of version 14, but is fixed
in Safari Technology Preview 117 which I think will be Safari 14.2.
https://bugs.webkit.org/show_bug.cgi?id=218076
*/
:target {
/* Safari does not support logical properties */
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
}
.page {
outline: 0;
padding: 0 var(--page-padding);
margin-block-start: calc(0px - var(--menu-bar-height)); /* Compensate for the #mdbook-menu-bar-hover-placeholder */
}
.page-wrapper {
box-sizing: border-box;
background-color: var(--bg);
}
html:not(.js) .page-wrapper,
.js:not(.sidebar-resizing) .page-wrapper {
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
}
[dir=rtl]:not(.js) .page-wrapper,
[dir=rtl].js:not(.sidebar-resizing) .page-wrapper {
transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
}
.content {
overflow-y: auto;
padding: 0 5px 50px 5px;
}
.content main {
margin-inline-start: auto;
margin-inline-end: auto;
max-width: var(--content-max-width);
}
.content p { line-height: 1.45em; }
.content ol { line-height: 1.45em; }
.content ul { line-height: 1.45em; }
.content a { text-decoration: none; }
.content a:hover { text-decoration: underline; }
.content img, .content video { max-width: 100%; }
.content .header:link,
.content .header:visited {
color: var(--fg);
}
.content .header:link,
.content .header:visited:hover {
text-decoration: none;
}
table {
margin: 0 auto;
border-collapse: collapse;
}
table td {
padding: 3px 20px;
border: 1px var(--table-border-color) solid;
}
table thead {
background: var(--table-header-bg);
}
table thead td {
font-weight: 700;
border: none;
}
table thead th {
padding: 3px 20px;
}
table thead tr {
border: 1px var(--table-header-bg) solid;
}
/* Alternate background colors for rows */
table tbody tr:nth-child(2n) {
background: var(--table-alternate-bg);
}
blockquote {
margin: 20px 0;
padding: 0 20px;
color: var(--fg);
background-color: var(--quote-bg);
border-block-start: .1em solid var(--quote-border);
border-block-end: .1em solid var(--quote-border);
}
/* TODO: Remove .warning in a future version of mdbook, it is replaced by
blockquote tags. */
.warning {
margin: 20px;
padding: 0 20px;
border-inline-start: 2px solid var(--warning-border);
}
.warning:before {
position: absolute;
width: 3rem;
height: 3rem;
margin-inline-start: calc(-1.5rem - 21px);
content: "ⓘ";
text-align: center;
background-color: var(--bg);
color: var(--warning-border);
font-weight: bold;
font-size: 2rem;
}
blockquote .warning:before {
background-color: var(--quote-bg);
}
kbd {
background-color: var(--table-border-color);
border-radius: 4px;
border: solid 1px var(--theme-popup-border);
box-shadow: inset 0 -1px 0 var(--theme-hover);
display: inline-block;
font-size: var(--code-font-size);
font-family: var(--mono-font);
line-height: 10px;
padding: 4px 5px;
vertical-align: middle;
}
sup {
/* Set the line-height for superscript and footnote references so that there
isn't an awkward space appearing above lines that contain the footnote.
See https://github.com/rust-lang/mdBook/pull/2443#discussion_r1813773583
for an explanation.
*/
line-height: 0;
}
.footnote-definition {
font-size: 0.9em;
}
/* The default spacing for a list is a little too large. */
.footnote-definition ul,
.footnote-definition ol {
padding-left: 20px;
}
.footnote-definition > li {
/* Required to position the ::before target */
position: relative;
}
.footnote-definition > li:target {
scroll-margin-top: 50vh;
}
.footnote-reference:target {
scroll-margin-top: 50vh;
}
/* Draws a border around the footnote (including the marker) when it is selected.
TODO: If there are multiple linkbacks, highlight which one you just came
from so you know which one to click.
*/
.footnote-definition > li:target::before {
border: 2px solid var(--footnote-highlight);
border-radius: 6px;
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -32px;
pointer-events: none;
content: "";
}
/* Pulses the footnote reference so you can quickly see where you left off reading.
This could use some improvement.
*/
@media not (prefers-reduced-motion) {
.footnote-reference:target {
animation: fn-highlight 0.8s;
border-radius: 2px;
}
@keyframes fn-highlight {
from {
background-color: var(--footnote-highlight);
}
}
}
.tooltiptext {
position: absolute;
visibility: hidden;
color: #fff;
background-color: #333;
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
left: -8px; /* Half of the width of the icon */
top: -35px;
font-size: 0.8em;
text-align: center;
border-radius: 6px;
padding: 5px 8px;
margin: 5px;
z-index: 1000;
}
.tooltipped .tooltiptext {
visibility: visible;
}
.chapter li.part-title {
color: var(--sidebar-fg);
margin: 5px 0px;
font-weight: bold;
}
.result-no-output {
font-style: italic;
}
.fa-svg svg {
width: 1em;
height: 1em;
fill: currentColor;
margin-bottom: -0.1em;
}
dt {
font-weight: bold;
margin-top: 0.5em;
margin-bottom: 0.1em;
}
/* This uses a CSS counter to add numbers to definitions, but only if there is
more than one definition. */
dl, dt {
counter-reset: dd-counter;
}
/* When there is more than one definition, increment the counter. The first
selector selects the first definition, and the second one selects definitions
2 and beyond.*/
dd:has(+ dd), dd + dd {
counter-increment: dd-counter;
/* Use flex display to help with positioning the numbers when there is a p
tag inside the definition. */
display: flex;
align-items: flex-start;
}
/* Shows the counter for definitions. The first selector selects the first
definition, and the second one selections definitions 2 and beyond.*/
dd:has(+ dd)::before, dd + dd::before {
content: counter(dd-counter) ". ";
font-weight: 600;
display: inline-block;
margin-right: 0.5em;
}
dd > p {
/* For loose definitions that have a p tag inside, don't add a bunch of
space before the definition. */
margin-top: 0;
}
/* Remove some excess space from the bottom. */
.blockquote-tag p:last-child {
margin-bottom: 2px;
}
.blockquote-tag {
/* Add some padding to make the vertical bar a little taller than the text.*/
padding: 2px 0px 2px 20px;
/* Add a solid color bar on the left side. */
border-inline-start-style: solid;
border-inline-start-width: 4px;
/* Disable the background color from normal blockquotes . */
background-color: inherit;
/* Disable border blocks from blockquotes. */
border-block-start: none;
border-block-end: none;
}
.blockquote-tag-title svg {
fill: currentColor;
/* Add space between the icon and the title. */
margin-right: 8px;
}
.blockquote-tag-note {
border-inline-start-color: var(--blockquote-note-color);
}
.blockquote-tag-tip {
border-inline-start-color: var(--blockquote-tip-color);
}
.blockquote-tag-important {
border-inline-start-color: var(--blockquote-important-color);
}
.blockquote-tag-warning {
border-inline-start-color: var(--blockquote-warning-color);
}
.blockquote-tag-caution {
border-inline-start-color: var(--blockquote-caution-color);
}
.blockquote-tag-note > .blockquote-tag-title {
color: var(--blockquote-note-color);
}
.blockquote-tag-tip > .blockquote-tag-title {
color: var(--blockquote-tip-color);
}
.blockquote-tag-important > .blockquote-tag-title {
color: var(--blockquote-important-color);
}
.blockquote-tag-warning > .blockquote-tag-title {
color: var(--blockquote-warning-color);
}
.blockquote-tag-caution > .blockquote-tag-title {
color: var(--blockquote-caution-color);
}
.blockquote-tag-title {
/* Slightly increase the weight for more emphasis. */
font-weight: 600;
/* Vertically center the icon with the text. */
display: flex;
align-items: center;
/* Remove default large margins for a more compact display. */
margin: 2px 0 8px 0;
}
.blockquote-tag-title .fa-svg {
fill: currentColor;
/* Add some space between the icon and the text. */
margin-right: 8px;
}

View file

@ -1,857 +0,0 @@
'use strict';
/* global default_theme, default_dark_theme, default_light_theme, hljs, ClipboardJS */
// Fix back button cache problem
window.onunload = function() { };
// Global variable, shared between modules
function playground_text(playground, hidden = true) {
const code_block = playground.querySelector('code');
if (window.ace && code_block.classList.contains('editable')) {
const editor = window.ace.edit(code_block);
return editor.getValue();
} else if (hidden) {
return code_block.textContent;
} else {
return code_block.innerText;
}
}
/**
* Helper for global keypress handlers so they don't trigger when certain elements are active.
* @returns {boolean} True if the keypress handler should be skipped.
*/
function mdbook_something_else_has_focus(e) {
// Check composedPath in case the event happened from something generated
// from the shadowDOM.
const target = e.composedPath()[0] || e.target;
return /^(?:input|select|textarea)$/i.test(target.nodeName);
}
(function codeSnippets() {
function fetch_with_timeout(url, options, timeout = 6000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
]);
}
const playgrounds = Array.from(document.querySelectorAll('.playground'));
if (playgrounds.length > 0) {
fetch_with_timeout('https://play.rust-lang.org/meta/crates', {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
mode: 'cors',
})
.then(response => response.json())
.then(response => {
// get list of crates available in the rust playground
const playground_crates = response.crates.map(item => item['id']);
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
});
}
function handle_crate_list_update(playground_block, playground_crates) {
// update the play buttons after receiving the response
update_play_button(playground_block, playground_crates);
// and install on change listener to dynamically update ACE editors
if (window.ace) {
const code_block = playground_block.querySelector('code');
if (code_block.classList.contains('editable')) {
const editor = window.ace.edit(code_block);
editor.addEventListener('change', () => {
update_play_button(playground_block, playground_crates);
});
// add Ctrl-Enter command to execute rust code
editor.commands.addCommand({
name: 'run',
bindKey: {
win: 'Ctrl-Enter',
mac: 'Ctrl-Enter',
},
exec: _editor => run_rust_code(playground_block),
});
}
}
}
// updates the visibility of play button based on `no_run` class and
// used crates vs ones available on https://play.rust-lang.org
function update_play_button(pre_block, playground_crates) {
const play_button = pre_block.querySelector('.play-button');
// skip if code is `no_run`
if (pre_block.querySelector('code').classList.contains('no_run')) {
play_button.classList.add('hidden');
return;
}
// get list of `extern crate`'s from snippet
const txt = playground_text(pre_block);
const re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
const snippet_crates = [];
let item;
while (item = re.exec(txt)) {
snippet_crates.push(item[1]);
}
// check if all used crates are available on play.rust-lang.org
const all_available = snippet_crates.every(function(elem) {
return playground_crates.indexOf(elem) > -1;
});
if (all_available) {
play_button.classList.remove('hidden');
play_button.hidden = false;
} else {
play_button.classList.add('hidden');
}
}
function run_rust_code(code_block) {
let result_block = code_block.querySelector('.result');
if (!result_block) {
result_block = document.createElement('code');
result_block.className = 'result hljs language-bash';
code_block.append(result_block);
}
const text = playground_text(code_block);
const classes = code_block.querySelector('code').classList;
let edition = '2015';
classes.forEach(className => {
if (className.startsWith('edition')) {
edition = className.slice(7);
}
});
const params = {
version: 'stable',
optimize: '0',
code: text,
edition: edition,
};
if (text.indexOf('#![feature') !== -1) {
params.version = 'nightly';
}
result_block.innerText = 'Running...';
fetch_with_timeout('https://play.rust-lang.org/evaluate.json', {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
mode: 'cors',
body: JSON.stringify(params),
})
.then(response => response.json())
.then(response => {
if (response.result.trim() === '') {
result_block.innerText = 'No output';
result_block.classList.add('result-no-output');
} else {
result_block.innerText = response.result;
result_block.classList.remove('result-no-output');
}
})
.catch(error => result_block.innerText = 'Playground Communication: ' + error.message);
}
// Syntax highlighting Configuration
hljs.configure({
tabReplace: ' ', // 4 spaces
languages: [], // Languages used for auto-detection
});
const code_nodes = Array
.from(document.querySelectorAll('code'))
// Don't highlight `inline code` blocks in headers.
.filter(function(node) {
return !node.parentElement.classList.contains('header');
});
if (window.ace) {
// language-rust class needs to be removed for editable
// blocks or highlightjs will capture events
code_nodes
.filter(function(node) {
return node.classList.contains('editable');
})
.forEach(function(block) {
block.classList.remove('language-rust');
});
code_nodes
.filter(function(node) {
return !node.classList.contains('editable');
})
.forEach(function(block) {
hljs.highlightBlock(block);
});
} else {
code_nodes.forEach(function(block) {
hljs.highlightBlock(block);
});
}
// Adding the hljs class gives code blocks the color css
// even if highlighting doesn't apply
code_nodes.forEach(function(block) {
block.classList.add('hljs');
});
Array.from(document.querySelectorAll('code.hljs')).forEach(function(block) {
const lines = Array.from(block.querySelectorAll('.boring'));
// If no lines were hidden, return
if (!lines.length) {
return;
}
block.classList.add('hide-boring');
const buttons = document.createElement('div');
buttons.className = 'buttons';
buttons.innerHTML = '<button title="Show hidden lines" \
aria-label="Show hidden lines"></button>';
buttons.firstChild.innerHTML = document.getElementById('fa-eye').innerHTML;
// add expand button
const pre_block = block.parentNode;
pre_block.insertBefore(buttons, pre_block.firstChild);
buttons.firstChild.addEventListener('click', function(e) {
if (this.title === 'Show hidden lines') {
this.innerHTML = document.getElementById('fa-eye-slash').innerHTML;
this.title = 'Hide lines';
this.setAttribute('aria-label', e.target.title);
block.classList.remove('hide-boring');
} else if (this.title === 'Hide lines') {
this.innerHTML = document.getElementById('fa-eye').innerHTML;
this.title = 'Show hidden lines';
this.setAttribute('aria-label', e.target.title);
block.classList.add('hide-boring');
}
});
});
if (window.playground_copyable) {
Array.from(document.querySelectorAll('pre code')).forEach(function(block) {
const pre_block = block.parentNode;
if (!pre_block.classList.contains('playground')) {
let buttons = pre_block.querySelector('.buttons');
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
const clipButton = document.createElement('button');
clipButton.className = 'clip-button';
clipButton.title = 'Copy to clipboard';
clipButton.setAttribute('aria-label', clipButton.title);
clipButton.innerHTML = '<i class="tooltiptext"></i>';
buttons.insertBefore(clipButton, buttons.firstChild);
}
});
}
// Process playground code blocks
Array.from(document.querySelectorAll('.playground')).forEach(function(pre_block) {
// Add play button
let buttons = pre_block.querySelector('.buttons');
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
const runCodeButton = document.createElement('button');
runCodeButton.className = 'play-button';
runCodeButton.hidden = true;
runCodeButton.title = 'Run this code';
runCodeButton.setAttribute('aria-label', runCodeButton.title);
runCodeButton.innerHTML = document.getElementById('fa-play').innerHTML;
buttons.insertBefore(runCodeButton, buttons.firstChild);
runCodeButton.addEventListener('click', () => {
run_rust_code(pre_block);
});
if (window.playground_copyable) {
const copyCodeClipboardButton = document.createElement('button');
copyCodeClipboardButton.className = 'clip-button';
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
copyCodeClipboardButton.title = 'Copy to clipboard';
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
}
const code_block = pre_block.querySelector('code');
if (window.ace && code_block.classList.contains('editable')) {
const undoChangesButton = document.createElement('button');
undoChangesButton.className = 'reset-button';
undoChangesButton.title = 'Undo changes';
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
undoChangesButton.innerHTML +=
document.getElementById('fa-clock-rotate-left').innerHTML;
buttons.insertBefore(undoChangesButton, buttons.firstChild);
undoChangesButton.addEventListener('click', function() {
const editor = window.ace.edit(code_block);
editor.setValue(editor.originalCode);
editor.clearSelection();
});
}
});
})();
(function themes() {
const html = document.querySelector('html');
const themeToggleButton = document.getElementById('mdbook-theme-toggle');
const themePopup = document.getElementById('mdbook-theme-list');
const themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
const themeIds = [];
themePopup.querySelectorAll('button.theme').forEach(function(el) {
themeIds.push(el.id);
});
const stylesheets = {
ayuHighlight: document.querySelector('#mdbook-ayu-highlight-css'),
tomorrowNight: document.querySelector('#mdbook-tomorrow-night-css'),
highlight: document.querySelector('#mdbook-highlight-css'),
};
function showThemes() {
themePopup.style.display = 'block';
themeToggleButton.setAttribute('aria-expanded', true);
themePopup.querySelector('button#mdbook-theme-' + get_theme()).focus();
}
function updateThemeSelected() {
themePopup.querySelectorAll('.theme-selected').forEach(function(el) {
el.classList.remove('theme-selected');
});
const selected = get_saved_theme() ?? 'default_theme';
let element = themePopup.querySelector('button#mdbook-theme-' + selected);
if (element === null) {
// Fall back in case there is no "Default" item.
element = themePopup.querySelector('button#mdbook-theme-' + get_theme());
}
element.classList.add('theme-selected');
}
function hideThemes() {
themePopup.style.display = 'none';
themeToggleButton.setAttribute('aria-expanded', false);
themeToggleButton.focus();
}
function get_saved_theme() {
let theme = null;
try {
theme = localStorage.getItem('mdbook-theme');
} catch {
// ignore error.
}
return theme;
}
function delete_saved_theme() {
localStorage.removeItem('mdbook-theme');
}
function get_theme() {
const theme = get_saved_theme();
if (theme === null || theme === undefined || !themeIds.includes('mdbook-theme-' + theme)) {
if (typeof default_dark_theme === 'undefined') {
// A customized index.hbs might not define this, so fall back to
// old behavior of determining the default on page load.
return default_theme;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches
? default_dark_theme
: default_light_theme;
} else {
return theme;
}
}
let previousTheme = default_theme;
function set_theme(theme, store = true) {
let ace_theme;
if (theme === 'coal' || theme === 'navy') {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = false;
stylesheets.highlight.disabled = true;
ace_theme = 'ace/theme/tomorrow_night';
} else if (theme === 'ayu') {
stylesheets.ayuHighlight.disabled = false;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = true;
ace_theme = 'ace/theme/tomorrow_night';
} else {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = false;
ace_theme = 'ace/theme/dawn';
}
setTimeout(function() {
themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
}, 1);
if (window.ace && window.editors) {
window.editors.forEach(function(editor) {
editor.setTheme(ace_theme);
});
}
if (store) {
try {
localStorage.setItem('mdbook-theme', theme);
} catch {
// ignore error.
}
}
html.classList.remove(previousTheme);
html.classList.add(theme);
previousTheme = theme;
updateThemeSelected();
}
const query = window.matchMedia('(prefers-color-scheme: dark)');
query.onchange = function() {
set_theme(get_theme(), false);
};
// Set theme.
set_theme(get_theme(), false);
themeToggleButton.addEventListener('click', function() {
if (themePopup.style.display === 'block') {
hideThemes();
} else {
showThemes();
}
});
themePopup.addEventListener('click', function(e) {
let theme;
if (e.target.className === 'theme') {
theme = e.target.id;
} else if (e.target.parentElement.className === 'theme') {
theme = e.target.parentElement.id;
} else {
return;
}
theme = theme.replace(/^mdbook-theme-/, '');
if (theme === 'default_theme' || theme === null) {
delete_saved_theme();
set_theme(get_theme(), false);
} else {
set_theme(theme);
}
});
themePopup.addEventListener('focusout', function(e) {
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
if (!!e.relatedTarget &&
!themeToggleButton.contains(e.relatedTarget) &&
!themePopup.contains(e.relatedTarget)
) {
hideThemes();
}
});
// Should not be needed, but it works around an issue on macOS & iOS:
// https://github.com/rust-lang/mdBook/issues/628
document.addEventListener('click', function(e) {
if (themePopup.style.display === 'block' &&
!themeToggleButton.contains(e.target) &&
!themePopup.contains(e.target)
) {
hideThemes();
}
});
document.addEventListener('keydown', function(e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
return;
}
if (!themePopup.contains(e.target)) {
return;
}
let li;
switch (e.key) {
case 'Escape':
e.preventDefault();
hideThemes();
break;
case 'ArrowUp':
e.preventDefault();
li = document.activeElement.parentElement;
if (li && li.previousElementSibling) {
li.previousElementSibling.querySelector('button').focus();
}
break;
case 'ArrowDown':
e.preventDefault();
li = document.activeElement.parentElement;
if (li && li.nextElementSibling) {
li.nextElementSibling.querySelector('button').focus();
}
break;
case 'Home':
e.preventDefault();
themePopup.querySelector('li:first-child button').focus();
break;
case 'End':
e.preventDefault();
themePopup.querySelector('li:last-child button').focus();
break;
}
});
})();
(function sidebar() {
const sidebar = document.getElementById('mdbook-sidebar');
const sidebarLinks = document.querySelectorAll('#mdbook-sidebar a');
const sidebarToggleButton = document.getElementById('mdbook-sidebar-toggle');
const sidebarResizeHandle = document.getElementById('mdbook-sidebar-resize-handle');
const sidebarCheckbox = document.getElementById('mdbook-sidebar-toggle-anchor');
let firstContact = null;
/* Because we cannot change the `display` using only CSS after/before the transition, we
need JS to do it. We change the display to prevent the browsers search to find text inside
the collapsed sidebar. */
if (!document.documentElement.classList.contains('sidebar-visible')) {
sidebar.style.display = 'none';
}
sidebar.addEventListener('transitionend', () => {
/* We only change the display to "none" if we're collapsing the sidebar. */
if (!sidebarCheckbox.checked) {
sidebar.style.display = 'none';
}
});
sidebarToggleButton.addEventListener('click', () => {
/* To allow the sidebar expansion animation, we first need to put back the display. */
if (!sidebarCheckbox.checked) {
sidebar.style.display = '';
// Workaround for Safari skipping the animation when changing
// `display` and a transform in the same event loop. This forces a
// reflow after updating the display.
sidebar.offsetHeight;
}
});
function showSidebar() {
document.documentElement.classList.add('sidebar-visible');
Array.from(sidebarLinks).forEach(function(link) {
link.setAttribute('tabIndex', 0);
});
sidebarToggleButton.setAttribute('aria-expanded', true);
sidebar.setAttribute('aria-hidden', false);
try {
localStorage.setItem('mdbook-sidebar', 'visible');
} catch {
// Ignore error.
}
}
function hideSidebar() {
document.documentElement.classList.remove('sidebar-visible');
Array.from(sidebarLinks).forEach(function(link) {
link.setAttribute('tabIndex', -1);
});
sidebarToggleButton.setAttribute('aria-expanded', false);
sidebar.setAttribute('aria-hidden', true);
try {
localStorage.setItem('mdbook-sidebar', 'hidden');
} catch {
// Ignore error.
}
}
// Toggle sidebar
sidebarCheckbox.addEventListener('change', function sidebarToggle() {
if (sidebarCheckbox.checked) {
const current_width = parseInt(
document.documentElement.style.getPropertyValue('--sidebar-target-width'), 10);
if (current_width < 150) {
document.documentElement.style.setProperty('--sidebar-target-width', '150px');
}
showSidebar();
} else {
hideSidebar();
}
});
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
function initResize() {
window.addEventListener('mousemove', resize, false);
window.addEventListener('mouseup', stopResize, false);
document.documentElement.classList.add('sidebar-resizing');
}
function resize(e) {
let pos = e.clientX - sidebar.offsetLeft;
if (pos < 20) {
hideSidebar();
} else {
if (!document.documentElement.classList.contains('sidebar-visible')) {
showSidebar();
}
pos = Math.min(pos, window.innerWidth - 100);
document.documentElement.style.setProperty('--sidebar-target-width', pos + 'px');
}
}
//on mouseup remove windows functions mousemove & mouseup
function stopResize() {
document.documentElement.classList.remove('sidebar-resizing');
window.removeEventListener('mousemove', resize, false);
window.removeEventListener('mouseup', stopResize, false);
}
document.addEventListener('touchstart', function(e) {
firstContact = {
x: e.touches[0].clientX,
time: Date.now(),
};
}, { passive: true });
document.addEventListener('touchmove', function(e) {
if (!firstContact) {
return;
}
const curX = e.touches[0].clientX;
const xDiff = curX - firstContact.x,
tDiff = Date.now() - firstContact.time;
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300)) {
showSidebar();
} else if (xDiff < 0 && curX < 300) {
hideSidebar();
}
firstContact = null;
}
}, { passive: true });
})();
(function chapterNavigation() {
document.addEventListener('keydown', function(e) {
if (e.altKey ||
e.ctrlKey ||
e.metaKey ||
window.search && window.search.hasFocus() ||
mdbook_something_else_has_focus(e)
) {
return;
}
const html = document.querySelector('html');
function next() {
const nextButton = document.querySelector('.nav-chapters.next');
if (nextButton) {
window.location.href = nextButton.href;
}
}
function prev() {
const previousButton = document.querySelector('.nav-chapters.previous');
if (previousButton) {
window.location.href = previousButton.href;
}
}
function showHelp() {
const container = document.getElementById('mdbook-help-container');
const overlay = document.getElementById('mdbook-help-popup');
container.style.display = 'flex';
// Clicking outside the popup will dismiss it.
const mouseHandler = event => {
if (overlay.contains(event.target)) {
return;
}
if (event.button !== 0) {
return;
}
event.preventDefault();
event.stopPropagation();
document.removeEventListener('mousedown', mouseHandler);
hideHelp();
};
// Pressing esc will dismiss the popup.
const escapeKeyHandler = event => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
document.removeEventListener('keydown', escapeKeyHandler, true);
hideHelp();
}
};
document.addEventListener('keydown', escapeKeyHandler, true);
document.getElementById('mdbook-help-container')
.addEventListener('mousedown', mouseHandler);
}
function hideHelp() {
document.getElementById('mdbook-help-container').style.display = 'none';
}
// Usually needs the Shift key to be pressed
switch (e.key) {
case '?':
e.preventDefault();
showHelp();
break;
}
// Rest of the keys are only active when the Shift key is not pressed
if (e.shiftKey) {
return;
}
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
if (html.dir === 'rtl') {
prev();
} else {
next();
}
break;
case 'ArrowLeft':
e.preventDefault();
if (html.dir === 'rtl') {
next();
} else {
prev();
}
break;
}
});
})();
(function clipboard() {
const clipButtons = document.querySelectorAll('.clip-button');
function hideTooltip(elem) {
elem.firstChild.innerText = '';
elem.className = 'clip-button';
}
function showTooltip(elem, msg) {
elem.firstChild.innerText = msg;
elem.className = 'clip-button tooltipped';
}
const clipboardSnippets = new ClipboardJS('.clip-button', {
text: function(trigger) {
hideTooltip(trigger);
const playground = trigger.closest('pre');
return playground_text(playground, false);
},
});
Array.from(clipButtons).forEach(function(clipButton) {
clipButton.addEventListener('mouseout', function(e) {
hideTooltip(e.currentTarget);
});
});
clipboardSnippets.on('success', function(e) {
e.clearSelection();
showTooltip(e.trigger, 'Copied!');
});
clipboardSnippets.on('error', function(e) {
showTooltip(e.trigger, 'Clipboard error!');
});
})();
(function scrollToTop() {
const menuTitle = document.querySelector('.menu-title');
menuTitle.addEventListener('click', function() {
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
});
})();
(function controllMenu() {
const menu = document.getElementById('mdbook-menu-bar');
(function controllPosition() {
let scrollTop = document.scrollingElement.scrollTop;
let prevScrollTop = scrollTop;
const minMenuY = -menu.clientHeight - 50;
// When the script loads, the page can be at any scroll (e.g. if you refresh it).
menu.style.top = scrollTop + 'px';
// Same as parseInt(menu.style.top.slice(0, -2), but faster
let topCache = menu.style.top.slice(0, -2);
menu.classList.remove('sticky');
let stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
document.addEventListener('scroll', function() {
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
// `null` means that it doesn't need to be updated
let nextSticky = null;
let nextTop = null;
const scrollDown = scrollTop > prevScrollTop;
const menuPosAbsoluteY = topCache - scrollTop;
if (scrollDown) {
nextSticky = false;
if (menuPosAbsoluteY > 0) {
nextTop = prevScrollTop;
}
} else {
if (menuPosAbsoluteY > 0) {
nextSticky = true;
} else if (menuPosAbsoluteY < minMenuY) {
nextTop = prevScrollTop + minMenuY;
}
}
if (nextSticky === true && stickyCache === false) {
menu.classList.add('sticky');
stickyCache = true;
} else if (nextSticky === false && stickyCache === true) {
menu.classList.remove('sticky');
stickyCache = false;
}
if (nextTop !== null) {
menu.style.top = nextTop + 'px';
topCache = nextTop;
}
prevScrollTop = scrollTop;
}, { passive: true });
})();
(function controllBorder() {
function updateBorder() {
if (menu.offsetTop === 0) {
menu.classList.remove('bordered');
} else {
menu.classList.add('bordered');
}
}
updateBorder();
document.addEventListener('scroll', updateBorder, { passive: true });
})();
})();

View file

@ -1,558 +0,0 @@
'use strict';
/* global Mark, elasticlunr, path_to_root */
window.search = window.search || {};
(function search() {
// Search functionality
//
// You can use !hasFocus() to prevent keyhandling in your key
// event handlers while the user is typing their search.
if (!Mark || !elasticlunr) {
return;
}
// eslint-disable-next-line max-len
// IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(search, pos) {
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
};
}
const search_wrap = document.getElementById('mdbook-search-wrapper'),
searchbar_outer = document.getElementById('mdbook-searchbar-outer'),
searchbar = document.getElementById('mdbook-searchbar'),
searchresults = document.getElementById('mdbook-searchresults'),
searchresults_outer = document.getElementById('mdbook-searchresults-outer'),
searchresults_header = document.getElementById('mdbook-searchresults-header'),
searchicon = document.getElementById('mdbook-search-toggle'),
content = document.getElementById('mdbook-content'),
// SVG text elements don't render if inside a <mark> tag.
mark_exclude = ['text'],
marker = new Mark(content),
URL_SEARCH_PARAM = 'search',
URL_MARK_PARAM = 'highlight';
let current_searchterm = '',
doc_urls = [],
search_options = {
bool: 'AND',
expand: true,
fields: {
title: {boost: 1},
body: {boost: 1},
breadcrumbs: {boost: 0},
},
},
searchindex = null,
results_options = {
teaser_word_count: 30,
limit_results: 30,
},
teaser_count = 0;
function hasFocus() {
return searchbar === document.activeElement;
}
function removeChildren(elem) {
while (elem.firstChild) {
elem.removeChild(elem.firstChild);
}
}
// Helper to parse a url into its building blocks.
function parseURL(url) {
const a = document.createElement('a');
a.href = url;
return {
source: url,
protocol: a.protocol.replace(':', ''),
host: a.hostname,
port: a.port,
params: (function() {
const ret = {};
const seg = a.search.replace(/^\?/, '').split('&');
for (const part of seg) {
if (!part) {
continue;
}
const s = part.split('=');
ret[s[0]] = s[1];
}
return ret;
})(),
file: (a.pathname.match(/\/([^/?#]+)$/i) || ['', ''])[1],
hash: a.hash.replace('#', ''),
path: a.pathname.replace(/^([^/])/, '/$1'),
};
}
// Helper to recreate a url string from its building blocks.
function renderURL(urlobject) {
let url = urlobject.protocol + '://' + urlobject.host;
if (urlobject.port !== '') {
url += ':' + urlobject.port;
}
url += urlobject.path;
let joiner = '?';
for (const prop in urlobject.params) {
if (Object.prototype.hasOwnProperty.call(urlobject.params, prop)) {
url += joiner + prop + '=' + urlobject.params[prop];
joiner = '&';
}
}
if (urlobject.hash !== '') {
url += '#' + urlobject.hash;
}
return url;
}
// Helper to escape html special chars for displaying the teasers
const escapeHTML = (function() {
const MAP = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
'\'': '&#39;',
};
const repl = function(c) {
return MAP[c];
};
return function(s) {
return s.replace(/[&<>'"]/g, repl);
};
})();
function formatSearchMetric(count, searchterm) {
if (count === 1) {
return count + ' search result for \'' + searchterm + '\':';
} else if (count === 0) {
return 'No search results for \'' + searchterm + '\'.';
} else {
return count + ' search results for \'' + searchterm + '\':';
}
}
function formatSearchResult(result, searchterms) {
const teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
teaser_count++;
// The ?URL_MARK_PARAM= parameter belongs in between the page and the #heading-anchor
const url = doc_urls[result.ref].split('#');
if (url.length === 1) { // no anchor found
url.push('');
}
// encodeURIComponent escapes all chars that could allow an XSS except
// for '. Due to that we also manually replace ' with its url-encoded
// representation (%27).
const encoded_search = encodeURIComponent(searchterms.join(' ')).replace(/'/g, '%27');
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + encoded_search
+ '#' + url[1] + '" aria-details="mdbook-teaser_' + teaser_count + '">'
+ result.doc.breadcrumbs + '</a>'
+ '<span class="teaser" id="mdbook-teaser_' + teaser_count
+ '" aria-label="Search Result Teaser">' + teaser + '</span>';
}
function makeTeaser(body, searchterms) {
// The strategy is as follows:
// First, assign a value to each word in the document:
// Words that correspond to search terms (stemmer aware): 40
// Normal words: 2
// First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in <em>.
const stemmed_searchterms = searchterms.map(function(w) {
return elasticlunr.stemmer(w.toLowerCase());
});
const searchterm_weight = 40;
const weighted = []; // contains elements of ["word", weight, index_in_document]
// split in sentences, then words
const sentences = body.toLowerCase().split('. ');
let index = 0;
let value = 0;
let searchterm_found = false;
for (const sentenceindex in sentences) {
const words = sentences[sentenceindex].split(' ');
value = 8;
for (const wordindex in words) {
const word = words[wordindex];
if (word.length > 0) {
for (const searchtermindex in stemmed_searchterms) {
if (elasticlunr.stemmer(word).startsWith(
stemmed_searchterms[searchtermindex])
) {
value = searchterm_weight;
searchterm_found = true;
}
}
weighted.push([word, value, index]);
value = 2;
}
index += word.length;
index += 1; // ' ' or '.' if last word in sentence
}
index += 1; // because we split at a two-char boundary '. '
}
if (weighted.length === 0) {
return body;
}
const window_weight = [];
const window_size = Math.min(weighted.length, results_options.teaser_word_count);
let cur_sum = 0;
for (let wordindex = 0; wordindex < window_size; wordindex++) {
cur_sum += weighted[wordindex][1];
}
window_weight.push(cur_sum);
for (let wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
cur_sum -= weighted[wordindex][1];
cur_sum += weighted[wordindex + window_size][1];
window_weight.push(cur_sum);
}
let max_sum_window_index = 0;
if (searchterm_found) {
let max_sum = 0;
// backwards
for (let i = window_weight.length - 1; i >= 0; i--) {
if (window_weight[i] > max_sum) {
max_sum = window_weight[i];
max_sum_window_index = i;
}
}
} else {
max_sum_window_index = 0;
}
// add <em/> around searchterms
const teaser_split = [];
index = weighted[max_sum_window_index][2];
for (let i = max_sum_window_index; i < max_sum_window_index + window_size; i++) {
const word = weighted[i];
if (index < word[2]) {
// missing text from index to start of `word`
teaser_split.push(body.substring(index, word[2]));
index = word[2];
}
if (word[1] === searchterm_weight) {
teaser_split.push('<em>');
}
index = word[2] + word[0].length;
teaser_split.push(body.substring(word[2], index));
if (word[1] === searchterm_weight) {
teaser_split.push('</em>');
}
}
return teaser_split.join('');
}
function init(config) {
results_options = config.results_options;
search_options = config.search_options;
doc_urls = config.doc_urls;
searchindex = elasticlunr.Index.load(config.index);
searchbar_outer.classList.remove('searching');
searchbar.focus();
const searchterm = searchbar.value.trim();
if (searchterm !== '') {
searchbar.classList.add('active');
doSearch(searchterm);
}
}
function initSearchInteractions(config) {
// Set up events
searchicon.addEventListener('click', () => {
searchIconClickHandler();
}, false);
searchbar.addEventListener('keyup', () => {
searchbarKeyUpHandler();
}, false);
document.addEventListener('keydown', e => {
globalKeyHandler(e);
}, false);
// If the user uses the browser buttons, do the same as if a reload happened
window.onpopstate = () => {
doSearchOrMarkFromUrl();
};
// Suppress "submit" events so the page doesn't reload when the user presses Enter
document.addEventListener('submit', e => {
e.preventDefault();
}, false);
// If reloaded, do the search or mark again, depending on the current url parameters
doSearchOrMarkFromUrl();
// Exported functions
config.hasFocus = hasFocus;
}
initSearchInteractions(window.search);
function unfocusSearchbar() {
// hacky, but just focusing a div only works once
const tmp = document.createElement('input');
tmp.setAttribute('style', 'position: absolute; opacity: 0;');
searchicon.appendChild(tmp);
tmp.focus();
tmp.remove();
}
// On reload or browser history backwards/forwards events, parse the url and do search or mark
function doSearchOrMarkFromUrl() {
// Check current URL for search request
const url = parseURL(window.location.href);
if (Object.prototype.hasOwnProperty.call(url.params, URL_SEARCH_PARAM)
&& url.params[URL_SEARCH_PARAM] !== '') {
showSearch(true);
searchbar.value = decodeURIComponent(
(url.params[URL_SEARCH_PARAM] + '').replace(/\+/g, '%20'));
searchbarKeyUpHandler(); // -> doSearch()
} else {
showSearch(false);
}
if (Object.prototype.hasOwnProperty.call(url.params, URL_MARK_PARAM)) {
const words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
marker.mark(words, {
exclude: mark_exclude,
});
const markers = document.querySelectorAll('mark');
const hide = () => {
for (let i = 0; i < markers.length; i++) {
markers[i].classList.add('fade-out');
window.setTimeout(() => {
marker.unmark();
}, 300);
}
// also removes the `?URL_MARK_PARAM=` search param so that
// in-page navigation doesn't make highlights unexpectedly appear again
setSearchUrlParameters('', 'replace');
};
for (let i = 0; i < markers.length; i++) {
markers[i].addEventListener('click', hide);
}
}
}
// Eventhandler for keyevents on `document`
function globalKeyHandler(e) {
if (e.altKey ||
e.ctrlKey ||
e.metaKey ||
e.shiftKey ||
e.target.type === 'textarea' ||
e.target.type === 'text' ||
!hasFocus() && mdbook_something_else_has_focus(e)
) {
return;
}
if (e.key === 'Escape') {
e.preventDefault();
searchbar.classList.remove('active');
setSearchUrlParameters('',
searchbar.value.trim() !== '' ? 'push' : 'replace');
if (hasFocus()) {
unfocusSearchbar();
}
showSearch(false);
marker.unmark();
} else if (!hasFocus() && (e.key === 's' || e.key === '/')) {
e.preventDefault();
showSearch(true);
window.scrollTo(0, 0);
searchbar.select();
} else if (hasFocus() && (e.key === 'ArrowDown'
|| e.key === 'Enter')) {
e.preventDefault();
const first = searchresults.firstElementChild;
if (first !== null) {
unfocusSearchbar();
first.classList.add('focus');
if (e.key === 'Enter') {
window.location.assign(first.querySelector('a'));
}
}
} else if (!hasFocus() && (e.key === 'ArrowDown'
|| e.key === 'ArrowUp'
|| e.key === 'Enter')) {
// not `:focus` because browser does annoying scrolling
const focused = searchresults.querySelector('li.focus');
if (!focused) {
return;
}
e.preventDefault();
if (e.key === 'ArrowDown') {
const next = focused.nextElementSibling;
if (next) {
focused.classList.remove('focus');
next.classList.add('focus');
}
} else if (e.key === 'ArrowUp') {
focused.classList.remove('focus');
const prev = focused.previousElementSibling;
if (prev) {
prev.classList.add('focus');
} else {
searchbar.select();
}
} else { // Enter
window.location.assign(focused.querySelector('a'));
}
}
}
function loadSearchScript(url, id) {
if (document.getElementById(id)) {
return;
}
searchbar_outer.classList.add('searching');
const script = document.createElement('script');
script.src = url;
script.id = id;
script.onload = () => init(window.search);
script.onerror = error => {
console.error(`Failed to load \`${url}\`: ${error}`);
};
document.head.append(script);
}
function showSearch(yes) {
if (yes) {
loadSearchScript(
window.path_to_searchindex_js ||
path_to_root + '{{ resource "searchindex.js" }}',
'mdbook-search-index');
search_wrap.classList.remove('hidden');
searchicon.setAttribute('aria-expanded', 'true');
} else {
search_wrap.classList.add('hidden');
searchicon.setAttribute('aria-expanded', 'false');
const results = searchresults.children;
for (let i = 0; i < results.length; i++) {
results[i].classList.remove('focus');
}
}
}
function showResults(yes) {
if (yes) {
searchresults_outer.classList.remove('hidden');
} else {
searchresults_outer.classList.add('hidden');
}
}
// Eventhandler for search icon
function searchIconClickHandler() {
if (search_wrap.classList.contains('hidden')) {
showSearch(true);
window.scrollTo(0, 0);
searchbar.select();
} else {
showSearch(false);
}
}
// Eventhandler for keyevents while the searchbar is focused
function searchbarKeyUpHandler() {
const searchterm = searchbar.value.trim();
if (searchterm !== '') {
searchbar.classList.add('active');
doSearch(searchterm);
} else {
searchbar.classList.remove('active');
showResults(false);
removeChildren(searchresults);
}
setSearchUrlParameters(searchterm, 'push_if_new_search_else_replace');
// Remove marks
marker.unmark();
}
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and
// `#heading-anchor`. `action` can be one of "push", "replace",
// "push_if_new_search_else_replace" and replaces or pushes a new browser history item.
// "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
function setSearchUrlParameters(searchterm, action) {
const url = parseURL(window.location.href);
const first_search = !Object.prototype.hasOwnProperty.call(url.params, URL_SEARCH_PARAM);
if (searchterm !== '' || action === 'push_if_new_search_else_replace') {
url.params[URL_SEARCH_PARAM] = searchterm;
delete url.params[URL_MARK_PARAM];
url.hash = '';
} else {
delete url.params[URL_MARK_PARAM];
delete url.params[URL_SEARCH_PARAM];
}
// A new search will also add a new history item, so the user can go back
// to the page prior to searching. A updated search term will only replace
// the url.
if (action === 'push' || action === 'push_if_new_search_else_replace' && first_search ) {
history.pushState({}, document.title, renderURL(url));
} else if (action === 'replace' ||
action === 'push_if_new_search_else_replace' &&
!first_search
) {
history.replaceState({}, document.title, renderURL(url));
}
}
function doSearch(searchterm) {
// Don't search the same twice
if (current_searchterm === searchterm) {
return;
}
searchbar_outer.classList.add('searching');
if (searchindex === null) {
return;
}
current_searchterm = searchterm;
// Do the actual search
const results = searchindex.search(searchterm, search_options);
const resultcount = Math.min(results.length, results_options.limit_results);
// Display search metrics
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
// Clear and insert results
const searchterms = searchterm.split(' ');
removeChildren(searchresults);
for (let i = 0; i < resultcount ; i++) {
const resultElem = document.createElement('li');
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
searchresults.appendChild(resultElem);
}
// Display results
showResults(true);
searchbar_outer.classList.remove('searching');
}
// Exported functions
search.hasFocus = hasFocus;
})(window.search);

View file

@ -1,367 +0,0 @@
<!DOCTYPE HTML>
<html lang="{{ language }}" class="{{ default_theme }} sidebar-visible" dir="{{ text_direction }}">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>{{ title }}</title>
{{#if is_print }}
<meta name="robots" content="noindex">
{{/if}}
{{#if base_url}}
<base href="{{ base_url }}">
{{/if}}
<!-- Custom HTML head -->
{{> head}}
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
{{#if favicon_svg}}
<link rel="icon" href="{{ resource "favicon.svg" }}">
{{/if}}
{{#if favicon_png}}
<link rel="shortcut icon" href="{{ resource "favicon.png" }}">
{{/if}}
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
{{#if print_enable}}
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
{{/if}}
<!-- Fonts -->
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" id="mdbook-highlight-css" href="{{ resource "highlight.css" }}">
<link rel="stylesheet" id="mdbook-tomorrow-night-css" href="{{ resource "tomorrow-night.css" }}">
<link rel="stylesheet" id="mdbook-ayu-highlight-css" href="{{ resource "ayu-highlight.css" }}">
<!-- Custom theme stylesheets -->
{{#each additional_css}}
<link rel="stylesheet" href="{{ resource this }}">
{{/each}}
{{#if mathjax_support}}
<!-- MathJax -->
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}}
<!-- Provide site root and default themes to javascript -->
<script>
const path_to_root = "{{ path_to_root }}";
const default_light_theme = "{{ default_theme }}";
const default_dark_theme = "{{ preferred_dark_theme }}";
{{#if search_js}}
window.path_to_searchindex_js = "{{ resource "searchindex.js" }}";
{{/if}}
</script>
<!-- Start loading toc.js asap -->
<script src="{{ resource "toc.js" }}"></script>
</head>
<body>
<div id="mdbook-help-container">
<div id="mdbook-help-popup">
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
<div>
<p>Press <kbd>←</kbd> or <kbd>→</kbd> to navigate between chapters</p>
{{#if search_enabled}}
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
{{/if}}
<p>Press <kbd>?</kbd> to show this help</p>
<p>Press <kbd>Esc</kbd> to hide this help</p>
</div>
</div>
</div>
<div id="mdbook-body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
let theme = localStorage.getItem('mdbook-theme');
let sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
let theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
const html = document.documentElement;
html.classList.remove('{{ default_theme }}')
html.classList.add(theme);
html.classList.add("js");
</script>
<input type="checkbox" id="mdbook-sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
let sidebar = null;
const sidebar_toggle = document.getElementById("mdbook-sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
sidebar_toggle.checked = false;
}
if (sidebar === 'visible') {
sidebar_toggle.checked = true;
} else {
html.classList.remove('sidebar-visible');
}
</script>
<nav id="mdbook-sidebar" class="sidebar" aria-label="Table of contents">
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
</noscript>
<div id="mdbook-sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<div id="mdbook-page-wrapper" class="page-wrapper">
<div class="page">
{{> header}}
<div id="mdbook-menu-bar-hover-placeholder"></div>
<div id="mdbook-menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="mdbook-sidebar-toggle" class="icon-button" for="mdbook-sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="mdbook-sidebar">
{{fa "solid" "bars"}}
</label>
<button id="mdbook-theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="mdbook-theme-list">
{{fa "solid" "paintbrush"}}
</button>
<ul id="mdbook-theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-default_theme">Auto</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-ayu">Ayu</button></li>
</ul>
{{#if search_enabled}}
<button id="mdbook-search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="mdbook-searchbar">
{{fa "solid" "magnifying-glass"}}
</button>
{{/if}}
</div>
<h1 class="menu-title">{{ book_title }}</h1>
<div class="right-buttons">
{{#if print_enable}}
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
{{fa "solid" "print" "print-button"}}
</a>
{{/if}}
{{#if git_repository_url}}
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
{{fa git_repository_icon_class git_repository_icon}}
</a>
{{/if}}
{{#if git_repository_edit_url}}
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit" rel="edit">
{{fa "solid" "pencil" "git-edit-button"}}
</a>
{{/if}}
</div>
</div>
{{#if search_enabled}}
<div id="mdbook-search-wrapper" class="hidden">
<form id="mdbook-searchbar-outer" class="searchbar-outer">
<div class="search-wrapper">
<input type="search" id="mdbook-searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="mdbook-searchresults-outer" aria-describedby="searchresults-header">
<div class="spinner-wrapper">
{{fa "solid" "spinner" "fa-spin"}}
</div>
</div>
</form>
<div id="mdbook-searchresults-outer" class="searchresults-outer hidden">
<div id="mdbook-searchresults-header" class="searchresults-header"></div>
<ul id="mdbook-searchresults">
</ul>
</div>
</div>
{{/if}}
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('mdbook-sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('mdbook-sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#mdbook-sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="mdbook-content" class="content">
<main>
{{{ content }}}
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
{{#if previous}}
<a rel="prev" href="{{ path_to_root }}{{previous.link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
{{#if (eq ../text_direction "rtl")}}
{{fa "solid" "angle-right"}}
{{else}}
{{fa "solid" "angle-left"}}
{{/if}}
</a>
{{/if}}
{{#if next}}
<a rel="next prefetch" href="{{ path_to_root }}{{next.link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
{{#if (eq ../text_direction "rtl")}}
{{fa "solid" "angle-left"}}
{{else}}
{{fa "solid" "angle-right"}}
{{/if}}
</a>
{{/if}}
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
{{#if previous}}
<a rel="prev" href="{{ path_to_root }}{{previous.link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
{{#if (eq ../text_direction "rtl")}}
{{fa "solid" "angle-right"}}
{{else}}
{{fa "solid" "angle-left"}}
{{/if}}
</a>
{{/if}}
{{#if next}}
<a rel="next prefetch" href="{{ path_to_root }}{{next.link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
{{#if (eq text_direction "rtl")}}
{{fa "solid" "angle-left"}}
{{else}}
{{fa "solid" "angle-right"}}
{{/if}}
</a>
{{/if}}
</nav>
</div>
<template id=fa-eye>{{fa "solid" "eye"}}</template>
<template id=fa-eye-slash>{{fa "solid" "eye-slash"}}</template>
<template id=fa-copy>{{fa "regular" "copy"}}</template>
<template id=fa-play>{{fa "solid" "play"}}</template>
<template id=fa-clock-rotate-left>{{fa "solid" "clock-rotate-left"}}</template>
{{#if live_reload_endpoint}}
<!-- Livereload script (if served using the cli tool) -->
<script>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
location.reload();
}
};
window.onbeforeunload = function() {
socket.close();
}
</script>
{{/if}}
{{#if playground_line_numbers}}
<script>
window.playground_line_numbers = true;
</script>
{{/if}}
{{#if playground_copyable}}
<script>
window.playground_copyable = true;
</script>
{{/if}}
{{#if playground_js}}
<script src="{{ resource "ace.js" }}"></script>
<script src="{{ resource "mode-rust.js" }}"></script>
<script src="{{ resource "editor.js" }}"></script>
<script src="{{ resource "theme-dawn.js" }}"></script>
<script src="{{ resource "theme-tomorrow_night.js" }}"></script>
{{/if}}
{{#if search_js}}
<script src="{{ resource "elasticlunr.min.js" }}"></script>
<script src="{{ resource "mark.min.js" }}"></script>
<script src="{{ resource "searcher.js" }}"></script>
{{/if}}
<script src="{{ resource "clipboard.min.js" }}"></script>
<script src="{{ resource "highlight.js" }}"></script>
<script src="{{ resource "book.js" }}"></script>
<!-- Custom JS scripts -->
{{#each additional_js}}
<script src="{{ resource this}}"></script>
{{/each}}
{{#if is_print}}
{{#if mathjax_support}}
<script>
window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100);
});
});
</script>
{{else}}
<script>
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
</script>
{{/if}}
{{/if}}
{{#if fragment_map}}
<script>
document.addEventListener('DOMContentLoaded', function() {
const fragmentMap =
{{{fragment_map}}}
;
const target = fragmentMap[window.location.hash];
if (target) {
let url = new URL(target, window.location.href);
window.location.replace(url.href);
}
});
</script>
{{/if}}
</div>
</body>
</html>

View file

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0; URL={{url}}">
<link rel="canonical" href="{{url}}">
</head>
<body>
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
<script>
// This handles redirects that involve fragments.
document.addEventListener('DOMContentLoaded', function() {
const fragmentMap =
{{{fragment_map}}}
;
const fragment = window.location.hash;
if (fragment) {
let redirectUrl = "{{url}}";
const target = fragmentMap[fragment];
if (target) {
let url = new URL(target, window.location.href);
redirectUrl = url.href;
} else {
let url = new URL(redirectUrl, window.location.href);
url.hash = window.location.hash;
redirectUrl = url.href;
}
window.location.replace(redirectUrl);
}
// else redirect handled by http-equiv
});
</script>
</body>
</html>

View file

@ -1,40 +0,0 @@
<!DOCTYPE HTML>
<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
<head>
<!-- sidebar iframe generated using mdBook
This is a frame, and not included directly in the page, to control the total size of the
book. The TOC contains an entry for each page, so if each page includes a copy of the TOC,
the total size of the page becomes O(n**2).
The frame is only used as a fallback when JS is turned off. When it's on, the sidebar is
instead added to the main page by `toc.js` instead. The JavaScript mode is better
because, when running in a `file:///` URL, the iframed page would not be Same-Origin as
the rest of the page, so the sidebar and the main page theme would fall out of sync.
-->
<meta charset="UTF-8">
<meta name="robots" content="noindex">
{{#if base_url}}
<base href="{{ base_url }}">
{{/if}}
<!-- Custom HTML head -->
{{> head}}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
{{#if print_enable}}
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
{{/if}}
<!-- Fonts -->
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
<!-- Custom theme stylesheets -->
{{#each additional_css}}
<link rel="stylesheet" href="{{ resource this }}">
{{/each}}
</head>
<body class="sidebar-iframe-inner">
{{#toc}}{{/toc}}
</body>
</html>

View file

@ -1,457 +0,0 @@
// Populate the sidebar
//
// This is a script, and not included directly in the page, to control the total size of the book.
// The TOC contains an entry for each page, so if each page includes a copy of the TOC,
// the total size of the page becomes O(n**2).
class MDBookSidebarScrollbox extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = '{{#toc}}{{/toc}}';
// Set the current, active page, and reveal it if it's hidden
let current_page = document.location.href.toString().split('#')[0].split('?')[0];
if (current_page.endsWith('/')) {
current_page += 'index.html';
}
const links = Array.prototype.slice.call(this.querySelectorAll('a'));
const l = links.length;
for (let i = 0; i < l; ++i) {
const link = links[i];
const href = link.getAttribute('href');
if (href && !href.startsWith('#') && !/^(?:[a-z+]+:)?\/\//.test(href)) {
link.href = path_to_root + href;
}
// The 'index' page is supposed to alias the first chapter in the book.
// Check both with and without the '.html' suffix to be robust against pretty URLs
if (link.href.replace(/\.html$/, '') === current_page.replace(/\.html$/, '')
|| i === 0
&& path_to_root === ''
&& current_page.endsWith('/index.html')) {
link.classList.add('active');
let parent = link.parentElement;
while (parent) {
if (parent.tagName === 'LI' && parent.classList.contains('chapter-item')) {
parent.classList.add('expanded');
}
parent = parent.parentElement;
}
}
}
// Track and set sidebar scroll position
this.addEventListener('click', e => {
if (e.target.tagName === 'A') {
const clientRect = e.target.getBoundingClientRect();
const sidebarRect = this.getBoundingClientRect();
sessionStorage.setItem('sidebar-scroll-offset', clientRect.top - sidebarRect.top);
}
}, { passive: true });
const sidebarScrollOffset = sessionStorage.getItem('sidebar-scroll-offset');
sessionStorage.removeItem('sidebar-scroll-offset');
if (sidebarScrollOffset !== null) {
// preserve sidebar scroll position when navigating via links within sidebar
const activeSection = this.querySelector('.active');
if (activeSection) {
const clientRect = activeSection.getBoundingClientRect();
const sidebarRect = this.getBoundingClientRect();
const currentOffset = clientRect.top - sidebarRect.top;
this.scrollTop += currentOffset - parseFloat(sidebarScrollOffset);
}
} else {
// scroll sidebar to current active section when navigating via
// 'next/previous chapter' buttons
const activeSection = document.querySelector('#mdbook-sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
// Toggle buttons
const sidebarAnchorToggles = document.querySelectorAll('.chapter-fold-toggle');
function toggleSection(ev) {
ev.currentTarget.parentElement.parentElement.classList.toggle('expanded');
}
Array.from(sidebarAnchorToggles).forEach(el => {
el.addEventListener('click', toggleSection);
});
}
}
window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox);
{{#if sidebar_header_nav}}
// ---------------------------------------------------------------------------
// Support for dynamically adding headers to the sidebar.
(function() {
// This is used to detect which direction the page has scrolled since the
// last scroll event.
let lastKnownScrollPosition = 0;
// This is the threshold in px from the top of the screen where it will
// consider a header the "current" header when scrolling down.
const defaultDownThreshold = 150;
// Same as defaultDownThreshold, except when scrolling up.
const defaultUpThreshold = 300;
// The threshold is a virtual horizontal line on the screen where it
// considers the "current" header to be above the line. The threshold is
// modified dynamically to handle headers that are near the bottom of the
// screen, and to slightly offset the behavior when scrolling up vs down.
let threshold = defaultDownThreshold;
// This is used to disable updates while scrolling. This is needed when
// clicking the header in the sidebar, which triggers a scroll event. It
// is somewhat finicky to detect when the scroll has finished, so this
// uses a relatively dumb system of disabling scroll updates for a short
// time after the click.
let disableScroll = false;
// Array of header elements on the page.
let headers;
// Array of li elements that are initially collapsed headers in the sidebar.
// I'm not sure why eslint seems to have a false positive here.
// eslint-disable-next-line prefer-const
let headerToggles = [];
// This is a debugging tool for the threshold which you can enable in the console.
let thresholdDebug = false;
// Updates the threshold based on the scroll position.
function updateThreshold() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// The number of pixels below the viewport, at most documentHeight.
// This is used to push the threshold down to the bottom of the page
// as the user scrolls towards the bottom.
const pixelsBelow = Math.max(0, documentHeight - (scrollTop + windowHeight));
// The number of pixels above the viewport, at least defaultDownThreshold.
// Similar to pixelsBelow, this is used to push the threshold back towards
// the top when reaching the top of the page.
const pixelsAbove = Math.max(0, defaultDownThreshold - scrollTop);
// How much the threshold should be offset once it gets close to the
// bottom of the page.
const bottomAdd = Math.max(0, windowHeight - pixelsBelow - defaultDownThreshold);
let adjustedBottomAdd = bottomAdd;
// Adjusts bottomAdd for a small document. The calculation above
// assumes the document is at least twice the windowheight in size. If
// it is less than that, then bottomAdd needs to be shrunk
// proportional to the difference in size.
if (documentHeight < windowHeight * 2) {
const maxPixelsBelow = documentHeight - windowHeight;
const t = 1 - pixelsBelow / Math.max(1, maxPixelsBelow);
const clamp = Math.max(0, Math.min(1, t));
adjustedBottomAdd *= clamp;
}
let scrollingDown = true;
if (scrollTop < lastKnownScrollPosition) {
scrollingDown = false;
}
if (scrollingDown) {
// When scrolling down, move the threshold up towards the default
// downwards threshold position. If near the bottom of the page,
// adjustedBottomAdd will offset the threshold towards the bottom
// of the page.
const amountScrolledDown = scrollTop - lastKnownScrollPosition;
const adjustedDefault = defaultDownThreshold + adjustedBottomAdd;
threshold = Math.max(adjustedDefault, threshold - amountScrolledDown);
} else {
// When scrolling up, move the threshold down towards the default
// upwards threshold position. If near the bottom of the page,
// quickly transition the threshold back up where it normally
// belongs.
const amountScrolledUp = lastKnownScrollPosition - scrollTop;
const adjustedDefault = defaultUpThreshold - pixelsAbove
+ Math.max(0, adjustedBottomAdd - defaultDownThreshold);
threshold = Math.min(adjustedDefault, threshold + amountScrolledUp);
}
if (documentHeight <= windowHeight) {
threshold = 0;
}
if (thresholdDebug) {
const id = 'mdbook-threshold-debug-data';
let data = document.getElementById(id);
if (data === null) {
data = document.createElement('div');
data.id = id;
data.style.cssText = `
position: fixed;
top: 50px;
right: 10px;
background-color: 0xeeeeee;
z-index: 9999;
pointer-events: none;
`;
document.body.appendChild(data);
}
data.innerHTML = `
<table>
<tr><td>documentHeight</td><td>${documentHeight.toFixed(1)}</td></tr>
<tr><td>windowHeight</td><td>${windowHeight.toFixed(1)}</td></tr>
<tr><td>scrollTop</td><td>${scrollTop.toFixed(1)}</td></tr>
<tr><td>pixelsAbove</td><td>${pixelsAbove.toFixed(1)}</td></tr>
<tr><td>pixelsBelow</td><td>${pixelsBelow.toFixed(1)}</td></tr>
<tr><td>bottomAdd</td><td>${bottomAdd.toFixed(1)}</td></tr>
<tr><td>adjustedBottomAdd</td><td>${adjustedBottomAdd.toFixed(1)}</td></tr>
<tr><td>scrollingDown</td><td>${scrollingDown}</td></tr>
<tr><td>threshold</td><td>${threshold.toFixed(1)}</td></tr>
</table>
`;
drawDebugLine();
}
lastKnownScrollPosition = scrollTop;
}
function drawDebugLine() {
if (!document.body) {
return;
}
const id = 'mdbook-threshold-debug-line';
const existingLine = document.getElementById(id);
if (existingLine) {
existingLine.remove();
}
const line = document.createElement('div');
line.id = id;
line.style.cssText = `
position: fixed;
top: ${threshold}px;
left: 0;
width: 100vw;
height: 2px;
background-color: red;
z-index: 9999;
pointer-events: none;
`;
document.body.appendChild(line);
}
function mdbookEnableThresholdDebug() {
thresholdDebug = true;
updateThreshold();
drawDebugLine();
}
window.mdbookEnableThresholdDebug = mdbookEnableThresholdDebug;
// Updates which headers in the sidebar should be expanded. If the current
// header is inside a collapsed group, then it, and all its parents should
// be expanded.
function updateHeaderExpanded(currentA) {
// Add expanded to all header-item li ancestors.
let current = currentA.parentElement;
while (current) {
if (current.tagName === 'LI' && current.classList.contains('header-item')) {
current.classList.add('expanded');
}
current = current.parentElement;
}
}
// Updates which header is marked as the "current" header in the sidebar.
// This is done with a virtual Y threshold, where headers at or below
// that line will be considered the current one.
function updateCurrentHeader() {
if (!headers || !headers.length) {
return;
}
// Reset the classes, which will be rebuilt below.
const els = document.getElementsByClassName('current-header');
for (const el of els) {
el.classList.remove('current-header');
}
for (const toggle of headerToggles) {
toggle.classList.remove('expanded');
}
// Find the last header that is above the threshold.
let lastHeader = null;
for (const header of headers) {
const rect = header.getBoundingClientRect();
if (rect.top <= threshold) {
lastHeader = header;
} else {
break;
}
}
if (lastHeader === null) {
lastHeader = headers[0];
const rect = lastHeader.getBoundingClientRect();
const windowHeight = window.innerHeight;
if (rect.top >= windowHeight) {
return;
}
}
// Get the anchor in the summary.
const href = '#' + lastHeader.id;
const a = [...document.querySelectorAll('.header-in-summary')]
.find(element => element.getAttribute('href') === href);
if (!a) {
return;
}
a.classList.add('current-header');
updateHeaderExpanded(a);
}
// Updates which header is "current" based on the threshold line.
function reloadCurrentHeader() {
if (disableScroll) {
return;
}
updateThreshold();
updateCurrentHeader();
}
// When clicking on a header in the sidebar, this adjusts the threshold so
// that it is located next to the header. This is so that header becomes
// "current".
function headerThresholdClick(event) {
// See disableScroll description why this is done.
disableScroll = true;
setTimeout(() => {
disableScroll = false;
}, 100);
// requestAnimationFrame is used to delay the update of the "current"
// header until after the scroll is done, and the header is in the new
// position.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// Closest is needed because if it has child elements like <code>.
const a = event.target.closest('a');
const href = a.getAttribute('href');
const targetId = href.substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
threshold = targetElement.getBoundingClientRect().bottom;
updateCurrentHeader();
}
});
});
}
// Takes the nodes from the given head and copies them over to the
// destination, along with some filtering.
function filterHeader(source, dest) {
const clone = source.cloneNode(true);
clone.querySelectorAll('mark').forEach(mark => {
mark.replaceWith(...mark.childNodes);
});
dest.append(...clone.childNodes);
}
// Scans page for headers and adds them to the sidebar.
document.addEventListener('DOMContentLoaded', function() {
const activeSection = document.querySelector('#mdbook-sidebar .active');
if (activeSection === null) {
return;
}
const main = document.getElementsByTagName('main')[0];
headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6'))
.filter(h => h.id !== '' && h.children.length && h.children[0].tagName === 'A');
if (headers.length === 0) {
return;
}
// Build a tree of headers in the sidebar.
const stack = [];
const firstLevel = parseInt(headers[0].tagName.charAt(1));
for (let i = 1; i < firstLevel; i++) {
const ol = document.createElement('ol');
ol.classList.add('section');
if (stack.length > 0) {
stack[stack.length - 1].ol.appendChild(ol);
}
stack.push({level: i + 1, ol: ol});
}
// The level where it will start folding deeply nested headers.
const foldLevel = 3;
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
const level = parseInt(header.tagName.charAt(1));
const currentLevel = stack[stack.length - 1].level;
if (level > currentLevel) {
// Begin nesting to this level.
for (let nextLevel = currentLevel + 1; nextLevel <= level; nextLevel++) {
const ol = document.createElement('ol');
ol.classList.add('section');
const last = stack[stack.length - 1];
const lastChild = last.ol.lastChild;
// Handle the case where jumping more than one nesting
// level, which doesn't have a list item to place this new
// list inside of.
if (lastChild) {
lastChild.appendChild(ol);
} else {
last.ol.appendChild(ol);
}
stack.push({level: nextLevel, ol: ol});
}
} else if (level < currentLevel) {
while (stack.length > 1 && stack[stack.length - 1].level > level) {
stack.pop();
}
}
const li = document.createElement('li');
li.classList.add('header-item');
li.classList.add('expanded');
if (level < foldLevel) {
li.classList.add('expanded');
}
const span = document.createElement('span');
span.classList.add('chapter-link-wrapper');
const a = document.createElement('a');
span.appendChild(a);
a.href = '#' + header.id;
a.classList.add('header-in-summary');
filterHeader(header.children[0], a);
a.addEventListener('click', headerThresholdClick);
const nextHeader = headers[i + 1];
if (nextHeader !== undefined) {
const nextLevel = parseInt(nextHeader.tagName.charAt(1));
if (nextLevel > level && level >= foldLevel) {
const toggle = document.createElement('a');
toggle.classList.add('chapter-fold-toggle');
toggle.classList.add('header-toggle');
toggle.addEventListener('click', () => {
li.classList.toggle('expanded');
});
const toggleDiv = document.createElement('div');
toggleDiv.textContent = '❱';
toggle.appendChild(toggleDiv);
span.appendChild(toggle);
headerToggles.push(li);
}
}
li.appendChild(span);
const currentParent = stack[stack.length - 1];
currentParent.ol.appendChild(li);
}
const onThisPage = document.createElement('div');
onThisPage.classList.add('on-this-page');
onThisPage.append(stack[0].ol);
const activeItemSpan = activeSection.parentElement;
activeItemSpan.after(onThisPage);
});
document.addEventListener('DOMContentLoaded', reloadCurrentHeader);
document.addEventListener('scroll', reloadCurrentHeader, { passive: true });
})();
{{/if}}

View file

@ -1,26 +0,0 @@
use pulldown_cmark::BlockQuoteKind;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_NOTE: &str = r#"<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_TIP: &str = r#"<path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_IMPORTANT: &str = r#"<path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_WARNING: &str = r#"<path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_CAUTION: &str = r#"<path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>"#;
pub(crate) fn select_tag(kind: BlockQuoteKind) -> (&'static str, &'static str, &'static str) {
match kind {
BlockQuoteKind::Note => ("note", ICON_NOTE, "Note"),
BlockQuoteKind::Tip => ("tip", ICON_TIP, "Tip"),
BlockQuoteKind::Important => ("important", ICON_IMPORTANT, "Important"),
BlockQuoteKind::Warning => ("warning", ICON_WARNING, "Warning"),
BlockQuoteKind::Caution => ("caution", ICON_CAUTION, "Caution"),
}
}

View file

@ -1,193 +0,0 @@
//! Support for hiding code lines.
use crate::html::{Element, Node};
use ego_tree::{NodeId, Tree};
use html5ever::tendril::StrTendril;
use mdbook_core::static_regex;
use std::collections::HashMap;
/// Wraps hidden lines in a `<span>` for the given code block.
pub(crate) fn hide_lines(
tree: &mut Tree<Node>,
code_id: NodeId,
hidelines: &HashMap<String, String>,
) {
let mut node = tree.get_mut(code_id).unwrap();
let el = node.value().as_element().unwrap();
let classes: Vec<_> = el.attr("class").unwrap_or_default().split(' ').collect();
let language = classes
.iter()
.filter_map(|cls| cls.strip_prefix("language-"))
.next()
.unwrap_or_default()
.to_string();
let hideline_info = classes
.iter()
.filter_map(|cls| cls.strip_prefix("hidelines="))
.map(|prefix| prefix.to_string())
.next();
if let Some(mut child) = node.first_child()
&& let Node::Text(text) = child.value()
{
if language == "rust" {
let new_nodes = hide_lines_rust(text);
child.detach();
let root = tree.extend_tree(new_nodes);
let root_id = root.id();
let mut node = tree.get_mut(code_id).unwrap();
node.reparent_from_id_append(root_id);
} else {
// Use the prefix from the code block, else the prefix from config.
let hidelines_prefix = hideline_info
.as_deref()
.or_else(|| hidelines.get(&language).map(|p| p.as_str()));
if let Some(prefix) = hidelines_prefix {
let new_nodes = hide_lines_with_prefix(text, prefix);
child.detach();
let root = tree.extend_tree(new_nodes);
let root_id = root.id();
let mut node = tree.get_mut(code_id).unwrap();
node.reparent_from_id_append(root_id);
}
}
}
}
/// Wraps hidden lines in a `<span>` specifically for Rust code blocks.
fn hide_lines_rust(text: &StrTendril) -> Tree<Node> {
static_regex!(BORING_LINES_REGEX, r"^(\s*)#(.?)(.*)$");
let mut tree = Tree::new(Node::Fragment);
let mut root = tree.root_mut();
let mut lines = text.lines().peekable();
while let Some(line) = lines.next() {
// Don't include newline on the last line.
let newline = if lines.peek().is_none() { "" } else { "\n" };
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
if &caps[2] == "#" {
root.append(Node::Text(
format!("{}{}{}{newline}", &caps[1], &caps[2], &caps[3]).into(),
));
continue;
} else if matches!(&caps[2], "" | " ") {
let mut span = Element::new("span");
span.insert_attr("class", "boring".into());
let mut span = root.append(Node::Element(span));
span.append(Node::Text(
format!("{}{}{newline}", &caps[1], &caps[3]).into(),
));
continue;
}
}
root.append(Node::Text(format!("{line}{newline}").into()));
}
tree
}
/// Wraps hidden lines in a `<span>` tag for lines starting with the given prefix.
fn hide_lines_with_prefix(content: &str, prefix: &str) -> Tree<Node> {
let mut tree = Tree::new(Node::Fragment);
let mut root = tree.root_mut();
for line in content.lines() {
if line.trim_start().starts_with(prefix) {
let pos = line.find(prefix).unwrap();
let (ws, rest) = (&line[..pos], &line[pos + prefix.len()..]);
let mut span = Element::new("span");
span.insert_attr("class", "boring".into());
let mut span = root.append(Node::Element(span));
span.append(Node::Text(format!("{ws}{rest}\n").into()));
} else {
root.append(Node::Text(format!("{line}\n").into()));
}
}
tree
}
/// If this code text is missing an `fn main`, the wrap it with `fn main` in a
/// fashion similar to rustdoc, with the wrapper hidden.
pub(crate) fn wrap_rust_main(text: &str) -> Option<String> {
if !text.contains("fn main") && !text.contains("quick_main!") {
let (attrs, code) = partition_rust_source(text);
let newline = if code.is_empty() || code.ends_with('\n') {
""
} else {
"\n"
};
Some(format!(
"# #![allow(unused)]\n{attrs}# fn main() {{\n{code}{newline}# }}"
))
} else {
None
}
}
/// Splits Rust inner attributes from the given source string.
///
/// Returns `(inner_attrs, rest_of_code)`.
fn partition_rust_source(s: &str) -> (&str, &str) {
static_regex!(
HEADER_RE,
r"^(?mx)
(
(?:
^[ \t]*\#!\[.* (?:\r?\n)?
|
^\s* (?:\r?\n)?
)*
)"
);
let split_idx = match HEADER_RE.captures(s) {
Some(caps) => {
let attributes = &caps[1];
if attributes.trim().is_empty() {
// Don't include pure whitespace as an attribute. The
// whitespace in the regex is intended to handle multiple
// attributes *separated* by potential whitespace.
0
} else {
attributes.len()
}
}
None => 0,
};
s.split_at(split_idx)
}
#[test]
fn it_partitions_rust_source() {
assert_eq!(partition_rust_source(""), ("", ""));
assert_eq!(partition_rust_source("let x = 1;"), ("", "let x = 1;"));
assert_eq!(
partition_rust_source("fn main()\n{ let x = 1; }\n"),
("", "fn main()\n{ let x = 1; }\n")
);
assert_eq!(
partition_rust_source("#![allow(foo)]"),
("#![allow(foo)]", "")
);
assert_eq!(
partition_rust_source("#![allow(foo)]\n"),
("#![allow(foo)]\n", "")
);
assert_eq!(
partition_rust_source("#![allow(foo)]\nlet x = 1;"),
("#![allow(foo)]\n", "let x = 1;")
);
assert_eq!(
partition_rust_source(
"\n\
#![allow(foo)]\n\
\n\
#![allow(bar)]\n\
\n\
let x = 1;"
),
("\n#![allow(foo)]\n\n#![allow(bar)]\n\n", "let x = 1;")
);
assert_eq!(
partition_rust_source(" // Example"),
("", " // Example")
);
}

View file

@ -1,108 +0,0 @@
//! HTML rendering support.
//!
//! This module's primary entry point is [`render_markdown`] which will take
//! markdown text and render it to HTML. In summary, the general procedure of
//! that function is:
//!
//! 1. Use [`pulldown_cmark`] to parse the markdown and generate events.
//! 2. [`tree`] converts those events to a tree data structure.
//! 1. Parse HTML inside the markdown using [`tokenizer`].
//! 2. Apply various transformations to the tree data structure, such as adding header links.
//! 3. Serialize the tree to HTML in [`serialize()`].
use ego_tree::Tree;
use mdbook_core::book::{Book, Chapter};
use mdbook_core::config::{HtmlConfig, RustEdition};
use mdbook_markdown::{MarkdownOptions, new_cmark_parser};
use std::path::{Path, PathBuf};
mod admonitions;
mod hide_lines;
mod print;
mod serialize;
#[cfg(test)]
mod tests;
mod tokenizer;
mod tree;
pub(crate) use hide_lines::{hide_lines, wrap_rust_main};
pub(crate) use print::render_print_page;
pub(crate) use serialize::serialize;
pub(crate) use tree::{Element, Node};
/// Options for converting a single chapter's markdown to HTML.
pub(crate) struct HtmlRenderOptions<'a> {
/// Options for parsing markdown.
pub markdown_options: MarkdownOptions,
/// The chapter's location, relative to the `SUMMARY.md` file.
pub path: &'a Path,
/// The default Rust edition, used to set the proper class on the code blocks.
pub edition: Option<RustEdition>,
/// The [`HtmlConfig`], whose options affect how the HTML is generated.
pub config: &'a HtmlConfig,
}
impl<'a> HtmlRenderOptions<'a> {
/// Creates a new [`HtmlRenderOptions`].
pub(crate) fn new(
path: &'a Path,
config: &'a HtmlConfig,
edition: Option<RustEdition>,
) -> HtmlRenderOptions<'a> {
let mut markdown_options = MarkdownOptions::default();
markdown_options.smart_punctuation = config.smart_punctuation;
markdown_options.definition_lists = config.definition_lists;
markdown_options.admonitions = config.admonitions;
HtmlRenderOptions {
markdown_options,
path,
edition,
config,
}
}
}
/// Renders markdown to HTML.
pub(crate) fn render_markdown(text: &str, options: &HtmlRenderOptions<'_>) -> String {
let tree = build_tree(text, options);
let mut output = String::new();
serialize::serialize(&tree, &mut output);
output
}
/// Renders markdown to a [`Tree`].
fn build_tree(text: &str, options: &HtmlRenderOptions<'_>) -> Tree<Node> {
let events = new_cmark_parser(text, &options.markdown_options);
tree::MarkdownTreeBuilder::build(options, events)
}
/// The parsed chapter, and some information about the chapter.
pub(crate) struct ChapterTree<'book> {
pub(crate) chapter: &'book Chapter,
/// The path to the chapter relative to the root with the `.html` extension.
pub(crate) html_path: PathBuf,
/// The chapter tree.
pub(crate) tree: Tree<Node>,
}
/// Creates all of the [`ChapterTree`]s for the book.
pub(crate) fn build_trees<'book>(
book: &'book Book,
html_config: &HtmlConfig,
edition: Option<RustEdition>,
) -> Vec<ChapterTree<'book>> {
book.chapters()
.map(|ch| {
let path = ch.path.as_ref().unwrap();
let html_path = ch.path.as_ref().unwrap().with_extension("html");
let options = HtmlRenderOptions::new(path, html_config, edition);
let tree = build_tree(&ch.content, &options);
ChapterTree {
chapter: ch,
html_path,
tree,
}
})
.collect()
}

View file

@ -1,238 +0,0 @@
//! Support for generating the print page.
//!
//! The print page takes all the individual chapters (as `Tree<Node>`
//! elements) and modifies the chapters so that they work on a consolidated
//! print page, and then serializes it all as one HTML page.
use super::Node;
use crate::html::{ChapterTree, Element, serialize};
use crate::utils::{ToUrlPath, id_from_content, normalize_path, unique_id};
use mdbook_core::static_regex;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
/// Takes all the chapter trees, modifies them to be suitable to render for
/// the print page, and returns an string of all the chapters rendered to a
/// single HTML page.
pub(crate) fn render_print_page(
mut chapter_trees: Vec<ChapterTree<'_>>,
site_url: Option<&str>,
) -> String {
let (id_remap, mut id_counter) = make_ids_unique(&mut chapter_trees);
let path_to_root_id = make_root_id_map(&mut chapter_trees, &mut id_counter);
rewrite_links(&mut chapter_trees, &id_remap, &path_to_root_id, site_url);
let mut print_content = String::new();
for ChapterTree { tree, .. } in chapter_trees {
if !print_content.is_empty() {
// 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
// Add both two CSS properties because of the compatibility issue
print_content
.push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
}
serialize(&tree, &mut print_content);
}
print_content
}
/// Make all IDs unique, and create a map from old to new IDs.
///
/// The first map is a map of the chapter path to the IDs that were rewritten
/// in that chapter (old ID to new ID).
///
/// The second map is a map of every ID seen to the number of times it has
/// been seen. This is used to generate unique IDs.
fn make_ids_unique(
chapter_trees: &mut [ChapterTree<'_>],
) -> (HashMap<PathBuf, HashMap<String, String>>, HashSet<String>) {
let mut id_remap = HashMap::new();
let mut id_counter = HashSet::new();
for ChapterTree {
html_path, tree, ..
} in chapter_trees
{
for value in tree.values_mut() {
if let Node::Element(el) = value
&& let Some(id) = el.attr("id")
{
let new_id = unique_id(id, &mut id_counter);
if new_id != id {
let id = id.to_string();
el.insert_attr("id", new_id.clone().into());
let map: &mut HashMap<_, _> = id_remap.entry(html_path.clone()).or_default();
map.insert(id, new_id);
}
}
}
}
(id_remap, id_counter)
}
/// Generates a map of a chapter path to the ID of the top of the chapter.
///
/// If a chapter is missing an `h1` tag, then one is synthesized so that the
/// print output has something to link to.
fn make_root_id_map(
chapter_trees: &mut [ChapterTree<'_>],
id_counter: &mut HashSet<String>,
) -> HashMap<PathBuf, String> {
let mut path_to_root_id = HashMap::new();
for ChapterTree {
chapter,
html_path,
tree,
..
} in chapter_trees
{
let mut h1_found = false;
for value in tree.values_mut() {
if let Node::Element(el) = value {
if el.name() == "h1" {
if let Some(id) = el.attr("id") {
h1_found = true;
path_to_root_id.insert(html_path.clone(), id.to_string());
}
break;
} else if matches!(el.name(), "h2" | "h3" | "h4" | "h5" | "h6") {
// h1 not found.
break;
}
}
}
if !h1_found {
// Synthesize a root id to be able to link to the start of the page.
// TODO: This might want to be a warning? Chapters generally
// should start with an h1.
let mut h1 = Element::new("h1");
let id = id_from_content(&chapter.name);
let id = unique_id(&id, id_counter);
h1.insert_attr("id", id.clone().into());
let mut root = tree.root_mut();
let mut h1 = root.prepend(Node::Element(h1));
let mut a = Element::new("a");
a.insert_attr("href", format!("#{id}").into());
a.insert_attr("class", "header".into());
let mut a = h1.append(Node::Element(a));
a.append(Node::Text(chapter.name.clone().into()));
path_to_root_id.insert(html_path.clone(), id);
}
}
path_to_root_id
}
/// Rewrite links so that they point to IDs on the print page.
fn rewrite_links(
chapter_trees: &mut [ChapterTree<'_>],
id_remap: &HashMap<PathBuf, HashMap<String, String>>,
path_to_root_id: &HashMap<PathBuf, String>,
site_url: Option<&str>,
) {
static_regex!(
LINK,
r"(?x)
(?P<scheme>^[a-z][a-z0-9+.-]*:)?
(?P<path>[^\#]+)?
(?:\#(?P<anchor>.*))?"
);
// Rewrite path links to go to the appropriate place.
for ChapterTree {
html_path, tree, ..
} in chapter_trees
{
let base = html_path.parent().expect("path can't be empty");
for value in tree.values_mut() {
let Node::Element(el) = value else {
continue;
};
if !matches!(el.name(), "a" | "img") {
continue;
}
for attr in ["href", "src", "xlink:href"] {
let Some(dest) = el.attr(attr).map(str::to_string) else {
continue;
};
// Links emitted under `site-url` are absolute (`{site_url}path`)
// and root-relative (anchored at the book root). Strip the
// prefix so the path resolves against the print page like any
// other chapter link, instead of being skipped as a scheme.
let (search, root_relative) = match site_url {
Some(site_url) if dest.starts_with(site_url) => (&dest[site_url.len()..], true),
_ => (dest.as_str(), false),
};
let Some(caps) = LINK.captures(search) else {
continue;
};
if !root_relative && caps.name("scheme").is_some() {
continue;
}
// The lookup_key is the key to look up in the remap table.
let mut lookup_key = html_path.clone();
if let Some(href_path) = caps.name("path")
&& let href_path = href_path.as_str()
&& !href_path.is_empty()
{
if root_relative {
// The path is already relative to the book root.
lookup_key = normalize_path(Path::new(href_path));
} else {
lookup_key.pop();
lookup_key.push(href_path);
lookup_key = normalize_path(&lookup_key);
}
let is_a_chapter = path_to_root_id.contains_key(&lookup_key);
if !is_a_chapter {
// Not part of the print page; rebuild a link to the
// standalone resource, preserving the absolute form for
// `site-url` links and a print-relative path otherwise.
let mut link = if root_relative {
format!("{}{href_path}", site_url.unwrap_or_default())
} else {
normalize_path(&base.join(href_path)).to_url_path()
};
if let Some(anchor) = caps.name("anchor") {
link.push('#');
link.push_str(anchor.as_str());
}
el.insert_attr(attr, link.into());
continue;
}
}
let id = match caps.name("anchor") {
Some(anchor_id) => {
let anchor_id = anchor_id.as_str().to_string();
match id_remap.get(&lookup_key) {
Some(id_map) => match id_map.get(&anchor_id) {
Some(new_id) => new_id.clone(),
None => anchor_id,
},
None => {
// Assume the anchor goes to some non-remapped
// ID that already exists.
anchor_id
}
}
}
None => match path_to_root_id.get(&lookup_key) {
Some(id) => id.to_string(),
None => {
// This should be guaranteed that either the
// chapter itself is in the map (for anchor-only
// links), or the is_a_chapter check above.
panic!(
"internal error: expected `{lookup_key:?}` to be in \
root map (chapter path is `{html_path:?}`)"
);
}
},
};
el.insert_attr(attr, format!("#{id}").into());
}
}
}
}

View file

@ -1,112 +0,0 @@
//! Serializes the [`Node`] tree to an HTML string.
use super::tree::is_void_element;
use super::tree::{Element, Node};
use ego_tree::{Tree, iter::Edge};
use html5ever::{local_name, ns};
use mdbook_core::utils::{escape_html, escape_html_attribute};
use std::ops::Deref;
/// Serializes the given tree of [`Node`] elements to an HTML string.
pub(crate) fn serialize(tree: &Tree<Node>, output: &mut String) {
for edge in tree.root().traverse() {
match edge {
Edge::Open(node) => match node.value() {
Node::Element(el) => serialize_start(el, output),
Node::Text(text) => {
output.push_str(&escape_html(text));
}
Node::Comment(comment) => {
output.push_str("<!--");
output.push_str(comment);
output.push_str("-->");
}
Node::Fragment => {}
Node::RawData(html) => {
output.push_str(html);
}
},
Edge::Close(node) => {
if let Node::Element(el) = node.value() {
serialize_end(el, output);
}
}
}
}
}
/// Returns true if this HTML element wants a newline to keep the emitted
/// output more readable.
fn wants_pretty_html_newline(name: &str) -> bool {
matches!(name, |"blockquote"| "dd"
| "div"
| "dl"
| "dt"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "hr"
| "li"
| "ol"
| "p"
| "pre"
| "table"
| "tbody"
| "thead"
| "tr"
| "ul")
}
/// Emit the start tag of an element.
fn serialize_start(el: &Element, output: &mut String) {
let el_name = el.name();
if wants_pretty_html_newline(el_name) {
if !output.is_empty() {
if !output.ends_with('\n') {
output.push('\n');
}
}
}
output.push('<');
output.push_str(el_name);
for (attr_name, value) in &el.attrs {
output.push(' ');
match attr_name.ns {
ns!() => (),
ns!(xml) => output.push_str("xml:"),
ns!(xmlns) => {
if el.name.local != local_name!("xmlns") {
output.push_str("xmlns:");
}
}
ns!(xlink) => output.push_str("xlink:"),
_ => (), // TODO what should it do here?
}
output.push_str(attr_name.local.deref());
output.push_str("=\"");
output.push_str(&escape_html_attribute(&value));
output.push('"');
}
if el.self_closing {
output.push_str(" /");
}
output.push('>');
}
/// Emit the end tag of an element.
fn serialize_end(el: &Element, output: &mut String) {
// Void elements do not have an end tag.
if el.self_closing || is_void_element(el.name()) {
return;
}
let name = el.name();
output.push_str("</");
output.push_str(name);
output.push('>');
if wants_pretty_html_newline(name) {
output.push('\n');
}
}

View file

@ -1,53 +0,0 @@
use crate::html::tokenizer::parse_html;
use html5ever::tokenizer::{Tag, TagKind, Token};
// Basic tokenizer behavior of a script.
#[test]
fn parse_html_script() {
let script = r#"
if (3 < 5 > 10)
{
alert("The sky is falling!");
}
"#;
let t = format!("<script>{script}</script>");
let ts = parse_html(&t);
eprintln!("{ts:#?}",);
let mut output = String::new();
let mut in_script = false;
for t in ts {
match t {
Token::ParseError(e) => panic!("{e:?}"),
Token::CharacterTokens(s) => {
if in_script {
output.push_str(&s)
}
}
Token::TagToken(Tag {
kind: TagKind::StartTag,
..
}) => in_script = true,
Token::TagToken(Tag {
kind: TagKind::EndTag,
..
}) => in_script = false,
_ => {}
}
}
assert_eq!(output, script);
}
// What happens if a script doesn't end.
#[test]
fn parse_html_script_unclosed() {
let t = r#"<script>
// Test
"#;
let ts = parse_html(t);
eprintln!("{ts:#?}",);
for t in ts {
if let Token::ParseError(e) = t {
panic!("{e:?}",);
}
}
}

View file

@ -1,83 +0,0 @@
//! Support for parsing HTML.
//!
//! The primary entry point is [`parse_html`] which uses [`html5ever`] to
//! tokenize the input.
use html5ever::TokenizerResult;
use html5ever::tendril::ByteTendril;
use html5ever::tokenizer::states::RawKind;
use html5ever::tokenizer::{
BufferQueue, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer, TokenizerOpts,
};
use std::cell::RefCell;
/// Collector for HTML tokens.
#[derive(Default)]
struct TokenCollector {
/// Parsed HTML tokens.
tokens: RefCell<Vec<Token>>,
}
impl TokenSink for TokenCollector {
type Handle = ();
fn process_token(&self, token: Token, _line_number: u64) -> TokenSinkResult<()> {
match &token {
Token::DoctypeToken(_) => {}
Token::TagToken(tag) => {
let tag_name = tag.name.as_bytes();
// TODO: This could probably use special support for SVG and MathML.
if tag_name == b"script" {
match tag.kind {
TagKind::StartTag => {
self.tokens.borrow_mut().push(token);
return TokenSinkResult::RawData(RawKind::ScriptData);
}
TagKind::EndTag => {}
}
}
if tag_name == b"style" {
match tag.kind {
TagKind::StartTag => {
self.tokens.borrow_mut().push(token);
return TokenSinkResult::RawData(RawKind::Rawtext);
}
TagKind::EndTag => {}
}
}
self.tokens.borrow_mut().push(token);
}
Token::CommentToken(_) => {
self.tokens.borrow_mut().push(token);
}
Token::CharacterTokens(_) => {
self.tokens.borrow_mut().push(token);
}
Token::NullCharacterToken => {}
Token::EOFToken => {}
Token::ParseError(_) => {
self.tokens.borrow_mut().push(token);
}
}
TokenSinkResult::Continue
}
}
/// Parse HTML into tokens.
pub(crate) fn parse_html(html: &str) -> Vec<Token> {
let tendril: ByteTendril = html.as_bytes().into();
let mut queue = BufferQueue::default();
queue.push_back(tendril.try_reinterpret().unwrap());
let collector = TokenCollector::default();
let tok = Tokenizer::new(collector, TokenizerOpts::default());
let result = tok.feed(&mut queue);
assert_eq!(result, TokenizerResult::Done);
assert!(
queue.is_empty(),
"queue wasn't empty: {:?}",
queue.pop_front()
);
tok.end();
tok.sink.tokens.take()
}

File diff suppressed because it is too large Load diff

View file

@ -1,725 +0,0 @@
use super::helpers;
use super::static_files::StaticFiles;
use crate::html::ChapterTree;
use crate::html::{build_trees, render_markdown, serialize};
use crate::theme::Theme;
use crate::utils::ToUrlPath;
use anyhow::{Context, Result, bail};
use handlebars::Handlebars;
use mdbook_core::book::{Book, BookItem, Chapter};
use mdbook_core::config::{BookConfig, Config, HtmlConfig};
use mdbook_core::utils::fs;
use mdbook_renderer::{RenderContext, Renderer};
use serde_json::json;
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use tracing::error;
use tracing::{debug, info, trace, warn};
/// The HTML renderer for mdBook.
#[derive(Default)]
#[non_exhaustive]
pub struct HtmlHandlebars;
impl HtmlHandlebars {
/// Returns a new instance of [`HtmlHandlebars`].
pub fn new() -> Self {
HtmlHandlebars
}
fn render_chapter(
&self,
chapter_tree: &ChapterTree<'_>,
prev_ch: Option<&Chapter>,
next_ch: Option<&Chapter>,
mut ctx: RenderChapterContext<'_>,
) -> Result<()> {
// FIXME: This should be made DRY-er and rely less on mutable state
let ch = chapter_tree.chapter;
let path = ch.path.as_ref().unwrap();
// "print.html" is used for the print page.
if path == Path::new("print.md") {
bail!("{} is reserved for internal use", path.display());
};
if let Some(ref edit_url_template) = ctx.html_config.edit_url_template {
let full_path = ctx.book_config.src.to_str().unwrap_or_default().to_owned()
+ "/"
+ ch.source_path
.clone()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
let edit_url = edit_url_template.replace("{path}", &full_path);
ctx.data
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
}
let mut content = String::new();
serialize(&chapter_tree.tree, &mut content);
let ctx_path = path
.to_str()
.with_context(|| "Could not convert path to str")?;
let filepath = Path::new(&ctx_path).with_extension("html");
let book_title = ctx
.data
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
let title = if let Some(title) = ctx.chapter_titles.get(path) {
title.clone()
} else if book_title.is_empty() {
ch.name.clone()
} else {
ch.name.clone() + " - " + book_title
};
ctx.data.insert("path".to_owned(), json!(path));
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
// With `site-url` configured, every page roots its chrome, navigation,
// sidebar (via `toc.js`) and asset links at the absolute site URL
// instead of a depth-relative prefix.
let path_to_root = match &ctx.html_config.site_url {
Some(site_url) => site_url.clone(),
None => fs::path_to_root(path),
};
ctx.data
.insert("path_to_root".to_owned(), json!(path_to_root));
if let Some(ref section) = ch.number {
ctx.data
.insert("section".to_owned(), json!(section.to_string()));
}
let redirects = collect_redirects_for_path(&filepath, &ctx.html_config.redirect)?;
if !redirects.is_empty() {
ctx.data.insert(
"fragment_map".to_owned(),
json!(serde_json::to_string(&redirects)?),
);
}
let mut nav = |name: &str, ch: Option<&Chapter>| {
let Some(ch) = ch else { return };
let path = ch
.path
.as_ref()
.unwrap()
.with_extension("html")
.to_url_path();
let obj = json!( {
"title": ch.name,
"link": path,
});
ctx.data.insert(name.to_string(), obj);
};
nav("previous", prev_ch);
nav("next", next_ch);
// Render the handlebars template with the data
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
// Write to file
let out_path = ctx.destination.join(filepath);
fs::write(&out_path, rendered)?;
if prev_ch.is_none() {
ctx.data.insert("path".to_owned(), json!("index.md"));
let index_root = match &ctx.html_config.site_url {
Some(site_url) => site_url.clone(),
None => String::new(),
};
ctx.data
.insert("path_to_root".to_owned(), json!(index_root));
ctx.data.insert("is_index".to_owned(), json!(true));
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
debug!("Creating index.html from {}", ctx_path);
fs::write(ctx.destination.join("index.html"), rendered_index)?;
}
Ok(())
}
fn render_404(
&self,
ctx: &RenderContext,
html_config: &HtmlConfig,
src_dir: &Path,
handlebars: &mut Handlebars<'_>,
data: &mut serde_json::Map<String, serde_json::Value>,
) -> Result<()> {
let content_404 = if let Some(ref filename) = html_config.input_404 {
let path = src_dir.join(filename);
fs::read_to_string(&path).with_context(|| "failed to read the 404 input file")?
} 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() {
fs::read_to_string(&default_404_location)
.with_context(|| "failed to read the 404 input file")?
} else {
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
navigation bar or search to continue."
.to_string()
}
};
let options = crate::html::HtmlRenderOptions::new(
Path::new("404.md"),
html_config,
ctx.config.rust.edition,
);
let html_content_404 = render_markdown(&content_404, &options);
let mut data_404 = data.clone();
let base_url = if let Some(site_url) = &html_config.site_url {
site_url
} else {
debug!(
"HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
this to ensure the 404 page work correctly, especially if your site is hosted in a \
subdirectory on the HTTP server."
);
"/"
};
data_404.insert("base_url".to_owned(), json!(base_url));
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
data_404.insert("path".to_owned(), json!("404.md"));
data_404.insert("content".to_owned(), json!(html_content_404));
let mut title = String::from("Page not found");
if let Some(book_title) = &ctx.config.book.title {
title.push_str(" - ");
title.push_str(book_title);
}
data_404.insert("title".to_owned(), json!(title));
let rendered = handlebars.render("index", &data_404)?;
let output_file = ctx.destination.join(html_config.get_404_output_file());
fs::write(output_file, rendered)?;
debug!("Creating 404.html ✓");
Ok(())
}
fn render_print_page(
&self,
ctx: &RenderContext,
handlebars: &Handlebars<'_>,
data: &mut serde_json::Map<String, serde_json::Value>,
chapter_trees: Vec<ChapterTree<'_>>,
site_url: Option<&str>,
) -> Result<String> {
let print_content = crate::html::render_print_page(chapter_trees, site_url);
if let Some(ref title) = ctx.config.book.title {
data.insert("title".to_owned(), json!(title));
} else {
// Make sure that the Print chapter does not display the title from
// the last rendered chapter by removing it from its context
data.remove("title");
}
data.insert("is_print".to_owned(), json!(true));
data.insert("path".to_owned(), json!("print.md"));
data.insert("content".to_owned(), json!(print_content));
// Root the print page chrome, assets and sidebar at the absolute site
// URL when configured, matching the per-chapter behaviour.
let path_to_root = match site_url {
Some(site_url) => site_url.to_owned(),
None => fs::path_to_root(Path::new("print.md")),
};
data.insert("path_to_root".to_owned(), json!(path_to_root));
debug!("Render template");
let rendered = handlebars.render("index", &data)?;
Ok(rendered)
}
fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) {
handlebars.register_helper(
"toc",
Box::new(helpers::toc::RenderToc {
no_section_label: html_config.no_section_label,
}),
);
handlebars.register_helper("fa", Box::new(helpers::fontawesome::fa_helper));
}
fn emit_redirects(
&self,
root: &Path,
handlebars: &Handlebars<'_>,
redirects: &HashMap<String, String>,
) -> Result<()> {
if redirects.is_empty() {
return Ok(());
}
debug!("Emitting redirects");
let redirects = combine_fragment_redirects(redirects);
for (original, (dest, fragment_map)) in redirects {
// Note: all paths are relative to the build directory, so the
// leading slash in an absolute path means nothing (and would mess
// up `root.join(original)`).
let original = original.trim_start_matches('/');
let filename = root.join(original);
if filename.exists() {
// This redirect is handled by the in-page fragment mapper.
continue;
}
if dest.is_empty() {
bail!(
"redirect entry for `{original}` only has source paths with `#` fragments\n\
There must be an entry without the `#` fragment to determine the default \
destination."
);
}
debug!("Redirecting \"{}\"\"{}\"", original, dest);
self.emit_redirect(handlebars, &filename, &dest, &fragment_map)?;
}
Ok(())
}
fn emit_redirect(
&self,
handlebars: &Handlebars<'_>,
original: &Path,
destination: &str,
fragment_map: &BTreeMap<String, String>,
) -> Result<()> {
if let Some(parent) = original.parent() {
fs::create_dir_all(parent)?
}
let js_map = serde_json::to_string(fragment_map)?;
let ctx = json!({
"fragment_map": js_map,
"url": destination,
});
let rendered = handlebars.render("redirect", &ctx).with_context(|| {
format!(
"Unable to create a redirect file at `{}`",
original.display()
)
})?;
fs::write(original, rendered)?;
Ok(())
}
}
impl Renderer for HtmlHandlebars {
fn name(&self) -> &str {
"html"
}
fn render(&self, ctx: &RenderContext) -> Result<()> {
let book_config = &ctx.config.book;
let html_config = ctx.config.html_config().unwrap_or_default();
let src_dir = ctx.root.join(&ctx.config.book.src);
let destination = &ctx.destination;
let book = &ctx.book;
let build_dir = ctx.root.join(&ctx.config.build.build_dir);
if destination.exists() {
fs::remove_dir_content(destination)
.with_context(|| "Unable to remove stale HTML output")?;
}
trace!("render");
let mut handlebars = Handlebars::new();
let theme_dir = match html_config.theme {
Some(ref theme) => {
let dir = ctx.root.join(theme);
if !dir.is_dir() {
bail!("theme dir {} does not exist", dir.display());
}
dir
}
None => ctx.root.join("theme"),
};
let theme = Theme::new(theme_dir);
debug!("Register the index handlebars template");
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
debug!("Register the head handlebars template");
handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?;
debug!("Register the redirect handlebars template");
handlebars
.register_template_string("redirect", String::from_utf8(theme.redirect.clone())?)?;
debug!("Register the header handlebars template");
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
debug!("Register the toc handlebars template");
handlebars.register_template_string("toc_js", String::from_utf8(theme.toc_js.clone())?)?;
handlebars
.register_template_string("toc_html", String::from_utf8(theme.toc_html.clone())?)?;
debug!("Register handlebars helpers");
self.register_hbs_helpers(&mut handlebars, &html_config);
let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?;
let chapter_trees = build_trees(book, &html_config, ctx.config.rust.edition);
fs::create_dir_all(destination)
.with_context(|| "Unexpected error when constructing destination path")?;
let mut static_files = StaticFiles::new(&theme, &html_config, &ctx.root)?;
// Render search index
#[cfg(feature = "search")]
{
let default = mdbook_core::config::Search::default();
let search = html_config.search.as_ref().unwrap_or(&default);
if search.enable {
super::search::create_files(&search, &mut static_files, &chapter_trees)?;
}
}
debug!("Render toc js");
{
let rendered_toc = handlebars.render("toc_js", &data)?;
static_files.add_builtin("toc.js", rendered_toc.as_bytes());
debug!("Creating toc.js ✓");
}
if html_config.hash_files {
static_files.hash_files()?;
}
debug!("Copy static files");
let resource_helper = static_files
.write_files(&destination)
.with_context(|| "Unable to copy across static files")?;
handlebars.register_helper("resource", Box::new(resource_helper));
debug!("Render toc html");
{
data.insert("is_toc_html".to_owned(), json!(true));
data.insert("path".to_owned(), json!("toc.html"));
// The no-JS sidebar fallback iframe contains only root-relative
// chapter links; a `<base href>` of the site URL resolves them (and
// the iframe's own assets) absolutely. Scoped to this render so it
// does not leak into the per-chapter `data` clones below.
if let Some(site_url) = &html_config.site_url {
data.insert("base_url".to_owned(), json!(site_url));
}
let rendered_toc = handlebars.render("toc_html", &data)?;
fs::write(destination.join("toc.html"), rendered_toc)?;
debug!("Creating toc.html ✓");
data.remove("base_url");
data.remove("path");
data.remove("is_toc_html");
}
fs::write(
destination.join(".nojekyll"),
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
)?;
if let Some(cname) = &html_config.cname {
fs::write(destination.join("CNAME"), format!("{cname}\n"))?;
}
for (i, chapter_tree) in chapter_trees.iter().enumerate() {
let previous = (i != 0).then(|| chapter_trees[i - 1].chapter);
let next = (i != chapter_trees.len() - 1).then(|| chapter_trees[i + 1].chapter);
let ctx = RenderChapterContext {
handlebars: &handlebars,
destination: destination.to_path_buf(),
data: data.clone(),
book_config: book_config.clone(),
html_config: html_config.clone(),
chapter_titles: &ctx.chapter_titles,
};
self.render_chapter(chapter_tree, previous, next, ctx)?;
}
// Render 404 page
if html_config.input_404 != Some("".to_string()) {
self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?;
}
// Render the print version.
if html_config.print.enable {
let print_rendered = self.render_print_page(
ctx,
&handlebars,
&mut data,
chapter_trees,
html_config.site_url.as_deref(),
)?;
fs::write(destination.join("print.html"), print_rendered)?;
debug!("Creating print.html ✓");
}
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
.context("Unable to emit redirects")?;
// Copy all remaining files, avoid a recursive copy from/to the book build dir
fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?;
info!("HTML book written to `{}`", destination.display());
Ok(())
}
}
fn make_data(
root: &Path,
book: &Book,
config: &Config,
html_config: &HtmlConfig,
theme: &Theme,
) -> Result<serde_json::Map<String, serde_json::Value>> {
trace!("make_data");
let mut data = serde_json::Map::new();
data.insert(
"language".to_owned(),
json!(config.book.language.clone().unwrap_or_default()),
);
data.insert(
"text_direction".to_owned(),
json!(config.book.realized_text_direction()),
);
data.insert(
"book_title".to_owned(),
json!(config.book.title.clone().unwrap_or_default()),
);
data.insert(
"description".to_owned(),
json!(config.book.description.clone().unwrap_or_default()),
);
if theme.favicon_png.is_some() {
data.insert("favicon_png".to_owned(), json!("favicon.png"));
}
if theme.favicon_svg.is_some() {
data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
}
if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint {
data.insert(
"live_reload_endpoint".to_owned(),
json!(live_reload_endpoint),
);
}
let default_theme = match html_config.default_theme {
Some(ref theme) => theme.to_lowercase(),
None => "light".to_string(),
};
data.insert("default_theme".to_owned(), json!(default_theme));
let preferred_dark_theme = match html_config.preferred_dark_theme {
Some(ref theme) => theme.to_lowercase(),
None => "navy".to_string(),
};
data.insert(
"preferred_dark_theme".to_owned(),
json!(preferred_dark_theme),
);
if html_config.mathjax_support {
data.insert("mathjax_support".to_owned(), json!(true));
}
// Add check to see if there is an additional style
if !html_config.additional_css.is_empty() {
let mut css = Vec::new();
for style in &html_config.additional_css {
match style.strip_prefix(root) {
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
}
}
data.insert("additional_css".to_owned(), json!(css));
}
// Add check to see if there is an additional script
if !html_config.additional_js.is_empty() {
let mut js = Vec::new();
for script in &html_config.additional_js {
match script.strip_prefix(root) {
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
Err(_) => js.push(script.to_str().expect("Could not convert to str")),
}
}
data.insert("additional_js".to_owned(), json!(js));
}
if html_config.playground.editable && html_config.playground.copy_js {
data.insert("playground_js".to_owned(), json!(true));
if html_config.playground.line_numbers {
data.insert("playground_line_numbers".to_owned(), json!(true));
}
}
if html_config.playground.copyable {
data.insert("playground_copyable".to_owned(), json!(true));
}
data.insert("print_enable".to_owned(), json!(html_config.print.enable));
data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
data.insert("fold_level".to_owned(), json!(html_config.fold.level));
data.insert(
"sidebar_header_nav".to_owned(),
json!(html_config.sidebar_header_nav),
);
let search = html_config.search.clone();
if cfg!(feature = "search") {
let search = search.unwrap_or_default();
data.insert("search_enabled".to_owned(), json!(search.enable));
data.insert(
"search_js".to_owned(),
json!(search.enable && search.copy_js),
);
} else if search.is_some() {
warn!("mdBook compiled without search support, ignoring `output.html.search` table");
warn!(
"please reinstall with `cargo install mdbook --force --features search`to use the \
search feature"
)
}
if let Some(ref git_repository_url) = html_config.git_repository_url {
data.insert("git_repository_url".to_owned(), json!(git_repository_url));
}
let git_repository_icon = match html_config.git_repository_icon {
Some(ref git_repository_icon) => git_repository_icon,
None => "fab-github",
};
let git_repository_icon_class = match git_repository_icon.split('-').next() {
Some("fa") => "regular",
Some("fas") => "solid",
Some("fab") => "brands",
_ => "regular",
};
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
data.insert(
"git_repository_icon_class".to_owned(),
json!(git_repository_icon_class),
);
let mut chapters = vec![];
for item in book.iter() {
// Create the data to inject in the template
let mut chapter = BTreeMap::new();
match *item {
BookItem::PartTitle(ref title) => {
chapter.insert("part".to_owned(), json!(title));
}
BookItem::Chapter(ref ch) => {
if let Some(ref section) = ch.number {
chapter.insert("section".to_owned(), json!(section.to_string()));
}
chapter.insert(
"has_sub_items".to_owned(),
json!((!ch.sub_items.is_empty()).to_string()),
);
chapter.insert("name".to_owned(), json!(ch.name));
if let Some(ref path) = ch.path {
let p = path
.to_str()
.with_context(|| "Could not convert path to str")?;
chapter.insert("path".to_owned(), json!(p));
}
}
BookItem::Separator => {
chapter.insert("spacer".to_owned(), json!("_spacer_"));
}
}
chapters.push(chapter);
}
data.insert("chapters".to_owned(), json!(chapters));
debug!("[*]: JSON constructed");
Ok(data)
}
struct RenderChapterContext<'a> {
handlebars: &'a Handlebars<'a>,
destination: PathBuf,
data: serde_json::Map<String, serde_json::Value>,
book_config: BookConfig,
html_config: HtmlConfig,
chapter_titles: &'a HashMap<PathBuf, String>,
}
/// Redirect mapping.
///
/// The key is the source path (like `foo/bar.html`). The value is a tuple
/// `(destination_path, fragment_map)`. The `destination_path` is the page to
/// redirect to. `fragment_map` is the map of fragments that override the
/// destination. For example, a fragment `#foo` could redirect to any other
/// page or site.
type CombinedRedirects = BTreeMap<String, (String, BTreeMap<String, String>)>;
fn combine_fragment_redirects(redirects: &HashMap<String, String>) -> CombinedRedirects {
let mut combined: CombinedRedirects = BTreeMap::new();
// This needs to extract the fragments to generate the fragment map.
for (original, new) in redirects {
if let Some((source_path, source_fragment)) = original.rsplit_once('#') {
let e = combined.entry(source_path.to_string()).or_default();
if let Some(old) = e.1.insert(format!("#{source_fragment}"), new.clone()) {
error!(
"internal error: found duplicate fragment redirect \
{old} for {source_path}#{source_fragment}"
);
}
} else {
let e = combined.entry(original.to_string()).or_default();
e.0 = new.clone();
}
}
combined
}
/// Collects fragment redirects for an existing page.
///
/// The returned map has keys like `#foo` and the value is the new destination
/// path or URL.
fn collect_redirects_for_path(
path: &Path,
redirects: &HashMap<String, String>,
) -> Result<BTreeMap<String, String>> {
let path = format!("/{}", path.to_url_path());
if redirects.contains_key(&path) {
bail!(
"redirect found for existing chapter at `{path}`\n\
Either delete the redirect or remove the chapter."
);
}
let key_prefix = format!("{path}#");
let map = redirects
.iter()
.filter_map(|(source, dest)| {
source
.strip_prefix(&key_prefix)
.map(|fragment| (format!("#{fragment}"), dest.to_string()))
})
.collect();
Ok(map)
}

View file

@ -1,57 +0,0 @@
use font_awesome_as_a_crate as fa;
use handlebars::{
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason,
};
use std::str::FromStr;
use tracing::trace;
pub(crate) fn fa_helper(
h: &Helper<'_>,
_r: &Handlebars<'_>,
_ctx: &Context,
_rc: &mut RenderContext<'_, '_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
trace!("fa_helper (handlebars helper)");
let type_ = h
.param(0)
.and_then(|v| v.value().as_str())
.and_then(|v| fa::Type::from_str(v).ok())
.ok_or_else(|| {
RenderErrorReason::Other(
"Param 0 with String type is required for fontawesome helper.".to_owned(),
)
})?;
let name = h.param(1).and_then(|v| v.value().as_str()).ok_or_else(|| {
RenderErrorReason::Other(
"Param 1 with String type is required for fontawesome helper.".to_owned(),
)
})?;
trace!("fa_helper: {} {}", type_, name);
let name = name
.strip_prefix("fa-")
.or_else(|| name.strip_prefix("fab-"))
.or_else(|| name.strip_prefix("fas-"))
.unwrap_or(name);
if let Some(id) = h.param(2).and_then(|v| v.value().as_str()) {
out.write(&format!("<span class=fa-svg id=\"{}\">", id))?;
} else {
out.write("<span class=fa-svg>")?;
}
out.write(fa::svg(type_, name).map_err(|_| {
let valid_types = "fas (solid), fab (brands), or far (regular)";
RenderErrorReason::Other(format!(
"Unknown Font Awesome icon `{name}` for type `{type_}`. \
Hint: check the icon name and prefix ({valid_types}) at \
https://fontawesome.com/v6/search?m=free"
))
})?)?;
out.write("</span>")?;
Ok(())
}

View file

@ -1,3 +0,0 @@
pub(crate) mod fontawesome;
pub(crate) mod resources;
pub(crate) mod toc;

View file

@ -1,60 +0,0 @@
use std::collections::HashMap;
use mdbook_core::utils;
use handlebars::{
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
};
// Handlebars helper to find filenames with hashes in them
#[derive(Clone)]
pub(crate) struct ResourceHelper {
pub hash_map: HashMap<String, String>,
}
impl HelperDef for ResourceHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'rc>,
_r: &'reg Handlebars<'_>,
ctx: &'rc Context,
rc: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
RenderErrorReason::Other(
"Param 0 with String type is required for resource helper.".to_owned(),
)
})?;
// Honor an explicit `path_to_root` from the render data when present
// (the `site-url` feature sets it to the absolute site root). Fall back
// to deriving it from the page path, which is the depth-relative prefix
// used for ordinary builds.
let path_to_root = match rc.evaluate(ctx, "@root/path_to_root") {
Ok(value) => value
.as_json()
.as_str()
.map(|s| s.replace('"', ""))
.unwrap_or_default(),
Err(_) => String::new(),
};
let path_to_root = if path_to_root.is_empty() {
let base_path = rc
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
.ok_or_else(|| {
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
})?
.replace("\"", "");
utils::fs::path_to_root(&base_path)
} else {
path_to_root
};
out.write(&path_to_root)?;
out.write(self.hash_map.get(param).map(|p| &p[..]).unwrap_or(&param))?;
Ok(())
}
}

View file

@ -1,445 +0,0 @@
use super::static_files::StaticFiles;
use crate::html::{ChapterTree, Node};
use crate::theme::searcher;
use crate::utils::ToUrlPath;
use anyhow::{Result, bail};
use ego_tree::iter::Edge;
use elasticlunr::{Index, IndexBuilder};
use mdbook_core::book::Chapter;
use mdbook_core::config::{Search, SearchChapterSettings};
use mdbook_core::static_regex;
use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
const MAX_WORD_LENGTH_TO_INDEX: usize = 80;
/// Tokenizes in the same way as elasticlunr-rs (for English), but also drops long tokens.
fn tokenize(text: &str) -> Vec<String> {
text.split(|c: char| c.is_whitespace() || c == '-')
.filter(|s| !s.is_empty())
.map(|s| s.trim().to_lowercase())
.filter(|s| s.len() <= MAX_WORD_LENGTH_TO_INDEX)
.collect()
}
/// Creates all files required for search.
pub(super) fn create_files(
search_config: &Search,
static_files: &mut StaticFiles,
chapter_trees: &[ChapterTree<'_>],
) -> Result<()> {
let mut index = IndexBuilder::new()
.add_field_with_tokenizer("title", Box::new(&tokenize))
.add_field_with_tokenizer("body", Box::new(&tokenize))
.add_field_with_tokenizer("breadcrumbs", Box::new(&tokenize))
.build();
// These are links to all of the headings in all of the chapters.
let mut doc_urls = Vec::new();
let chapter_configs = sort_search_config(&search_config.chapter);
validate_chapter_config(&chapter_configs, chapter_trees)?;
for ct in chapter_trees {
let path = settings_path(ct.chapter);
let chapter_settings = get_chapter_settings(&chapter_configs, path);
if !chapter_settings.enable.unwrap_or(true) {
continue;
}
index_chapter(&mut index, search_config, &mut doc_urls, ct)?;
}
let index = write_to_json(index, search_config, doc_urls)?;
debug!("Writing search index ✓");
if index.len() > 10_000_000 {
warn!("search index is very large ({} bytes)", index.len());
}
if search_config.copy_js {
static_files.add_builtin(
"searchindex.js",
// To reduce the size of the generated JSON by preventing all `"` characters to be
// escaped, we instead surround the string with much less common `'` character.
format!(
"window.search = Object.assign(window.search, JSON.parse('{}'));",
index.replace("\\", "\\\\").replace("'", "\\'")
)
.as_bytes(),
);
static_files.add_builtin("searcher.js", searcher::JS);
static_files.add_builtin("mark.min.js", searcher::MARK_JS);
static_files.add_builtin("elasticlunr.min.js", searcher::ELASTICLUNR_JS);
debug!("Copying search files ✓");
}
Ok(())
}
/// Uses the given arguments to construct a search document, then inserts it to the given index.
fn add_doc(
index: &mut Index,
doc_urls: &mut Vec<String>,
anchor_base: &str,
heading_id: &str,
items: &[&str],
) {
let mut url = anchor_base.to_string();
if !heading_id.is_empty() {
url.push('#');
url.push_str(heading_id);
}
let doc_ref = doc_urls.len().to_string();
doc_urls.push(url);
let items = items.iter().map(|&x| collapse_whitespace(x.trim()));
index.add_doc(&doc_ref, items);
}
/// Adds the chapter to the search index.
fn index_chapter(
index: &mut Index,
search_config: &Search,
doc_urls: &mut Vec<String>,
chapter_tree: &ChapterTree<'_>,
) -> Result<()> {
let anchor_base = chapter_tree.html_path.to_url_path();
let mut in_heading = false;
let max_section_depth = search_config.heading_split_level;
let mut section_id = None;
let mut heading = String::new();
let mut body = String::new();
let mut breadcrumbs = chapter_tree.chapter.parent_names.clone();
breadcrumbs.push(chapter_tree.chapter.name.clone());
let mut traverse = chapter_tree.tree.root().traverse();
while let Some(edge) = traverse.next() {
match edge {
Edge::Open(node) => match node.value() {
Node::Element(el) => {
if let Some(level) = el.heading_level()
&& level <= max_section_depth
&& let Some(heading_id) = el.attr("id")
{
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
add_doc(
index,
doc_urls,
&anchor_base,
section_id.unwrap(),
&[&heading, &body, &breadcrumbs.join(" » ")],
);
heading.clear();
body.clear();
breadcrumbs.pop();
}
section_id = Some(heading_id);
in_heading = true;
} else if matches!(el.name(), "script" | "style") {
// Skip this node.
while let Some(edge) = traverse.next() {
if let Edge::Close(close) = edge
&& close == node
{
break;
}
}
// Insert spaces where HTML output would usually separate text
// to ensure words don't get merged together
} else if in_heading {
heading.push(' ');
} else {
body.push(' ');
}
}
Node::Text(text) => {
if in_heading {
heading.push_str(text);
} else {
body.push_str(text);
}
}
Node::Comment(_) => {}
Node::Fragment => {}
Node::RawData(_) => {}
},
Edge::Close(node) => match node.value() {
Node::Element(el) => {
if let Some(level) = el.heading_level()
&& level <= max_section_depth
{
in_heading = false;
breadcrumbs.push(heading.clone());
}
}
_ => {}
},
}
}
if !body.is_empty() || !heading.is_empty() {
// Make sure the last section is added to the index
let title = if heading.is_empty() {
if let Some(chapter) = breadcrumbs.first() {
chapter
} else {
""
}
} else {
&heading
};
add_doc(
index,
doc_urls,
&anchor_base,
section_id.unwrap_or_default(),
&[title, &body, &breadcrumbs.join(" » ")],
);
}
Ok(())
}
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
use elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
use std::collections::BTreeMap;
#[derive(Serialize)]
struct ResultsOptions {
limit_results: u32,
teaser_word_count: u32,
}
#[derive(Serialize)]
struct SearchindexJson {
/// The options used for displaying search results
results_options: ResultsOptions,
/// The searchoptions for elasticlunr.js
search_options: SearchOptions,
/// Used to lookup a document's URL from an integer document ref.
doc_urls: Vec<String>,
/// The index for elasticlunr.js
index: elasticlunr::Index,
}
let mut fields = BTreeMap::new();
let mut opt = SearchOptionsField::default();
let mut insert_boost = |key: &str, boost| {
opt.boost = Some(boost);
fields.insert(key.into(), opt);
};
insert_boost("title", search_config.boost_title);
insert_boost("body", search_config.boost_paragraph);
insert_boost("breadcrumbs", search_config.boost_hierarchy);
let search_options = SearchOptions {
bool: if search_config.use_boolean_and {
SearchBool::And
} else {
SearchBool::Or
},
expand: search_config.expand,
fields,
};
let results_options = ResultsOptions {
limit_results: search_config.limit_results,
teaser_word_count: search_config.teaser_word_count,
};
let json_contents = SearchindexJson {
results_options,
search_options,
doc_urls,
index,
};
// By converting to serde_json::Value as an intermediary, we use a
// BTreeMap internally and can force a stable ordering of map keys.
let json_contents = serde_json::to_value(&json_contents)?;
let json_contents = serde_json::to_string(&json_contents)?;
Ok(json_contents)
}
fn settings_path(ch: &Chapter) -> &Path {
ch.source_path
.as_deref()
.unwrap_or_else(|| ch.path.as_deref().unwrap())
}
fn validate_chapter_config(
chapter_configs: &[(PathBuf, SearchChapterSettings)],
chapter_trees: &[ChapterTree<'_>],
) -> Result<()> {
for (path, _) in chapter_configs {
let found = chapter_trees
.iter()
.any(|ct| settings_path(ct.chapter).starts_with(path));
if !found {
bail!(
"[output.html.search.chapter] key `{}` does not match any chapter paths",
path.display()
);
}
}
Ok(())
}
fn sort_search_config(
map: &HashMap<String, SearchChapterSettings>,
) -> Vec<(PathBuf, SearchChapterSettings)> {
let mut settings: Vec<_> = map
.iter()
.map(|(key, value)| (PathBuf::from(key), value.clone()))
.collect();
// Note: This is case-sensitive, and assumes the author uses the same case
// as the actual filename.
settings.sort_by(|a, b| a.0.cmp(&b.0));
settings
}
fn get_chapter_settings(
chapter_configs: &[(PathBuf, SearchChapterSettings)],
source_path: &Path,
) -> SearchChapterSettings {
let mut result = SearchChapterSettings::default();
for (path, config) in chapter_configs {
if source_path.starts_with(path) {
result.enable = config.enable.or(result.enable);
}
}
result
}
/// Replaces multiple consecutive whitespace characters with a single space character.
fn collapse_whitespace(text: &str) -> Cow<'_, str> {
static_regex!(WS, r"\s\s+");
WS.replace_all(text, " ")
}
#[test]
fn chapter_settings_priority() {
let cfg = r#"
[output.html.search.chapter]
"cli/watch.md" = { enable = true }
"cli" = { enable = false }
"cli/inner/foo.md" = { enable = false }
"cli/inner" = { enable = true }
"foo" = {} # Just to make sure empty table is allowed.
"#;
let cfg: mdbook_core::config::Config = toml::from_str(cfg).unwrap();
let html = cfg.html_config().unwrap();
let chapter_configs = sort_search_config(&html.search.unwrap().chapter);
for (path, enable) in [
("foo.md", None),
("cli/watch.md", Some(true)),
("cli/index.md", Some(false)),
("cli/inner/index.md", Some(true)),
("cli/inner/foo.md", Some(false)),
] {
let mut settings = SearchChapterSettings::default();
settings.enable = enable;
assert_eq!(
get_chapter_settings(&chapter_configs, Path::new(path)),
settings
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tokenize_basic() {
assert_eq!(tokenize("hello world"), vec!["hello", "world"]);
}
#[test]
fn test_tokenize_with_hyphens() {
assert_eq!(
tokenize("hello-world test-case"),
vec!["hello", "world", "test", "case"]
);
}
#[test]
fn test_tokenize_mixed_whitespace() {
assert_eq!(
tokenize("hello\tworld\ntest\r\ncase"),
vec!["hello", "world", "test", "case"]
);
}
#[test]
fn test_tokenize_empty_string() {
assert_eq!(tokenize(""), Vec::<String>::new());
}
#[test]
fn test_tokenize_only_whitespace() {
assert_eq!(tokenize(" \t\n "), Vec::<String>::new());
}
#[test]
fn test_tokenize_case_normalization() {
assert_eq!(tokenize("Hello WORLD Test"), vec!["hello", "world", "test"]);
}
#[test]
fn test_tokenize_trim_whitespace() {
assert_eq!(tokenize(" hello world "), vec!["hello", "world"]);
}
#[test]
fn test_tokenize_long_words_filtered() {
let long_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX + 1);
let short_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX);
let input = format!("{} hello {}", long_word, short_word);
assert_eq!(tokenize(&input), vec!["hello", &short_word]);
}
#[test]
fn test_tokenize_max_length_word() {
let max_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX);
assert_eq!(tokenize(&max_word), vec![max_word]);
}
#[test]
fn test_tokenize_special_characters() {
assert_eq!(
tokenize("hello,world.test!case?"),
vec!["hello,world.test!case?"]
);
}
#[test]
fn test_tokenize_unicode() {
assert_eq!(
tokenize("café naïve résumé"),
vec!["café", "naïve", "résumé"]
);
}
#[test]
fn test_tokenize_unicode_rtl_hebre() {
assert_eq!(tokenize("שלום עולם"), vec!["שלום", "עולם"]);
}
#[test]
fn test_tokenize_numbers() {
assert_eq!(
tokenize("test123 456-789 hello"),
vec!["test123", "456", "789", "hello"]
);
}
}

View file

@ -1,320 +0,0 @@
//! Support for writing static files.
use super::helpers::resources::ResourceHelper;
use crate::theme::{self, Theme, playground_editor};
use anyhow::{Context, Result};
use mdbook_core::config::HtmlConfig;
use mdbook_core::static_regex;
use mdbook_core::utils::fs;
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::debug;
/// Map static files to their final names and contents.
///
/// It performs [fingerprinting], if you call the `hash_files` method.
/// If hash-files is turned off, then the files will not be renamed.
/// It also writes files to their final destination, when `write_files` is called,
/// and interprets the `{{ resource }}` directives to allow assets to name each other.
///
/// [fingerprinting]: https://guides.rubyonrails.org/asset_pipeline.html#fingerprinting-versioning-with-digest-based-urls
pub(super) struct StaticFiles {
static_files: Vec<StaticFile>,
hash_map: HashMap<String, String>,
}
enum StaticFile {
Builtin {
data: Vec<u8>,
filename: String,
},
Additional {
input_location: PathBuf,
filename: String,
},
}
impl StaticFiles {
pub(super) fn new(theme: &Theme, html_config: &HtmlConfig, root: &Path) -> Result<StaticFiles> {
let static_files = Vec::new();
let mut this = StaticFiles {
hash_map: HashMap::new(),
static_files,
};
this.add_builtin("book.js", &theme.js);
this.add_builtin("css/general.css", &theme.general_css);
this.add_builtin("css/chrome.css", &theme.chrome_css);
if html_config.print.enable {
this.add_builtin("css/print.css", &theme.print_css);
}
this.add_builtin("css/variables.css", &theme.variables_css);
if let Some(contents) = &theme.favicon_png {
this.add_builtin("favicon.png", contents);
}
if let Some(contents) = &theme.favicon_svg {
this.add_builtin("favicon.svg", contents);
}
this.add_builtin("highlight.css", &theme.highlight_css);
this.add_builtin("tomorrow-night.css", &theme.tomorrow_night_css);
this.add_builtin("ayu-highlight.css", &theme.ayu_highlight_css);
this.add_builtin("highlight.js", &theme.highlight_js);
this.add_builtin("clipboard.min.js", &theme.clipboard_js);
if theme.fonts_css.is_none() {
this.add_builtin("fonts/fonts.css", theme::fonts::CSS);
for (file_name, contents) in theme::fonts::LICENSES.iter() {
this.add_builtin(file_name, contents);
}
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
this.add_builtin(file_name, contents);
}
this.add_builtin(
theme::fonts::SOURCE_CODE_PRO.0,
theme::fonts::SOURCE_CODE_PRO.1,
);
} else if let Some(fonts_css) = &theme.fonts_css {
if !fonts_css.is_empty() {
this.add_builtin("fonts/fonts.css", fonts_css);
}
}
let playground_config = &html_config.playground;
// Ace is a very large dependency, so only load it when requested
if playground_config.editable && playground_config.copy_js {
// Load the editor
this.add_builtin("editor.js", playground_editor::JS);
this.add_builtin("ace.js", playground_editor::ACE_JS);
this.add_builtin("mode-rust.js", playground_editor::MODE_RUST_JS);
this.add_builtin("theme-dawn.js", playground_editor::THEME_DAWN_JS);
this.add_builtin(
"theme-tomorrow_night.js",
playground_editor::THEME_TOMORROW_NIGHT_JS,
);
}
let custom_files = html_config
.additional_css
.iter()
.chain(html_config.additional_js.iter());
for custom_file in custom_files {
let input_location = root.join(custom_file);
this.static_files.push(StaticFile::Additional {
input_location,
filename: custom_file
.to_str()
.with_context(|| "resource file names must be valid utf8")?
.to_owned(),
});
}
for input_location in theme.font_files.iter().cloned() {
let filename = Path::new("fonts")
.join(input_location.file_name().unwrap())
.to_str()
.with_context(|| "resource file names must be valid utf8")?
.to_owned();
this.static_files.push(StaticFile::Additional {
input_location,
filename,
});
}
Ok(this)
}
pub(super) fn add_builtin(&mut self, filename: &str, data: &[u8]) {
self.static_files.push(StaticFile::Builtin {
filename: filename.to_owned(),
data: data.to_owned(),
});
}
/// Updates this [`StaticFiles`] to hash the contents for determining the
/// filename for each resource.
pub(super) fn hash_files(&mut self) -> Result<()> {
use sha2::{Digest, Sha256};
use std::io::Read;
for static_file in &mut self.static_files {
match static_file {
&mut StaticFile::Builtin {
ref mut filename,
ref data,
} => {
let mut parts = filename.splitn(2, '.');
let parts = parts.next().and_then(|p| Some((p, parts.next()?)));
if let Some((name, suffix)) = parts {
if name != "" && suffix != "" && suffix != "txt" {
let hex = hex::encode(&Sha256::digest(data)[..4]);
let new_filename = format!("{}-{}.{}", name, hex, suffix);
self.hash_map.insert(filename.clone(), new_filename.clone());
*filename = new_filename;
}
}
}
&mut StaticFile::Additional {
ref mut filename,
ref input_location,
} => {
let mut parts = filename.splitn(2, '.');
let parts = parts.next().and_then(|p| Some((p, parts.next()?)));
if let Some((name, suffix)) = parts {
if name != "" && suffix != "" {
let mut digest = Sha256::new();
let mut input_file =
std::fs::File::open(input_location).with_context(|| {
format!("failed to open `{filename}` for hashing")
})?;
let mut buf = vec![0; 1024];
loop {
let amt = input_file
.read(&mut buf)
.with_context(|| "read static file for hashing")?;
if amt == 0 {
break;
};
digest.update(&buf[..amt]);
}
let hex = hex::encode(&digest.finalize()[..4]);
let new_filename = format!("{}-{}.{}", name, hex, suffix);
self.hash_map.insert(filename.clone(), new_filename.clone());
*filename = new_filename;
}
}
}
}
}
Ok(())
}
pub(super) fn write_files(self, destination: &Path) -> Result<ResourceHelper> {
use regex::bytes::Captures;
// The `{{ resource "name" }}` directive in static resources look like
// handlebars syntax, even if they technically aren't.
static_regex!(RESOURCE, bytes, r#"\{\{ resource "([^"]+)" \}\}"#);
fn replace_all<'a>(
hash_map: &HashMap<String, String>,
data: &'a [u8],
filename: &str,
) -> Cow<'a, [u8]> {
RESOURCE.replace_all(data, move |captures: &Captures<'_>| {
let name = captures
.get(1)
.expect("capture 1 in resource regex")
.as_bytes();
let name = std::str::from_utf8(name).expect("resource name with invalid utf8");
let resource_filename = hash_map.get(name).map(|s| &s[..]).unwrap_or(name);
let path_to_root = fs::path_to_root(filename);
format!("{}{}", path_to_root, resource_filename)
.as_bytes()
.to_owned()
})
}
for static_file in &self.static_files {
match static_file {
StaticFile::Builtin { filename, data } => {
debug!("Writing builtin -> {}", filename);
let data = if filename.ends_with(".css") || filename.ends_with(".js") {
replace_all(&self.hash_map, data, filename)
} else {
Cow::Borrowed(&data[..])
};
let path = destination.join(filename);
fs::write(path, &data)?;
}
StaticFile::Additional {
input_location,
filename,
} => {
let output_location = destination.join(filename);
debug!(
"Copying {} -> {}",
input_location.display(),
output_location.display()
);
if let Some(parent) = output_location.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Unable to create {}", parent.display()))?;
}
if filename.ends_with(".css") || filename.ends_with(".js") {
let data = fs::read_to_string(input_location)?;
let data = replace_all(&self.hash_map, data.as_bytes(), filename);
let path = destination.join(filename);
fs::write(path, &data)?;
} else {
std::fs::copy(input_location, &output_location).with_context(|| {
format!(
"Unable to copy {} to {}",
input_location.display(),
output_location.display()
)
})?;
}
}
}
}
let hash_map = self.hash_map;
Ok(ResourceHelper { hash_map })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::theme::Theme;
use mdbook_core::config::HtmlConfig;
use mdbook_core::utils::fs;
use tempfile::TempDir;
#[test]
fn test_write_directive() {
let theme = Theme {
index: Vec::new(),
head: Vec::new(),
redirect: Vec::new(),
header: Vec::new(),
chrome_css: Vec::new(),
general_css: Vec::new(),
print_css: Vec::new(),
variables_css: Vec::new(),
favicon_png: Some(Vec::new()),
favicon_svg: Some(Vec::new()),
js: Vec::new(),
highlight_css: Vec::new(),
tomorrow_night_css: Vec::new(),
ayu_highlight_css: Vec::new(),
highlight_js: Vec::new(),
clipboard_js: Vec::new(),
toc_js: Vec::new(),
toc_html: Vec::new(),
fonts_css: None,
font_files: Vec::new(),
};
let temp_dir = TempDir::with_prefix("mdbook-").unwrap();
let reference_js = Path::new("static-files-test-case-reference.js");
let mut html_config = HtmlConfig::default();
html_config.additional_js.push(reference_js.to_owned());
fs::write(
temp_dir.path().join(reference_js),
br#"{{ resource "book.js" }}"#,
)
.unwrap();
let mut static_files = StaticFiles::new(&theme, &html_config, temp_dir.path()).unwrap();
static_files.hash_files().unwrap();
static_files.write_files(temp_dir.path()).unwrap();
// custom JS winds up referencing book.js
let reference_js_content = fs::read_to_string(
temp_dir
.path()
.join("static-files-test-case-reference-635c9cdc.js"),
)
.unwrap();
assert_eq!("book-e3b0c442.js", reference_js_content);
// book.js winds up empty
let book_js_content = fs::read_to_string(temp_dir.path().join("book-e3b0c442.js")).unwrap();
assert_eq!("", book_js_content);
}
}

View file

@ -1,8 +0,0 @@
//! mdBook HTML renderer.
mod html;
mod html_handlebars;
pub mod theme;
pub(crate) mod utils;
pub use html_handlebars::HtmlHandlebars;

View file

@ -1,61 +0,0 @@
pub(crate) static CSS: &[u8] = include_bytes!("../../front-end/fonts/fonts.css");
// An array of (file_name, file_contents) pairs
pub(crate) static LICENSES: [(&str, &[u8]); 2] = [
(
"fonts/OPEN-SANS-LICENSE.txt",
include_bytes!("../../front-end/fonts/OPEN-SANS-LICENSE.txt"),
),
(
"fonts/SOURCE-CODE-PRO-LICENSE.txt",
include_bytes!("../../front-end/fonts/SOURCE-CODE-PRO-LICENSE.txt"),
),
];
// An array of (file_name, file_contents) pairs
pub(crate) static OPEN_SANS: [(&str, &[u8]); 10] = [
(
"fonts/open-sans-v17-all-charsets-300.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-300.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-300italic.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-300italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-regular.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-regular.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-italic.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-600.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-600.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-600italic.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-600italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-700.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-700.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-700italic.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-700italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-800.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-800.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-800italic.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-800italic.woff2"),
),
];
// A (file_name, file_contents) pair
pub(crate) static SOURCE_CODE_PRO: (&str, &[u8]) = (
"fonts/source-code-pro-v11-all-charsets-500.woff2",
include_bytes!("../../front-end/fonts/source-code-pro-v11-all-charsets-500.woff2"),
);

View file

@ -1,10 +0,0 @@
//! Theme dependencies for the playground editor.
pub(crate) static JS: &[u8] = include_bytes!("../../front-end/playground_editor/editor.js");
pub(crate) static ACE_JS: &[u8] = include_bytes!("../../front-end/playground_editor/ace.js");
pub(crate) static MODE_RUST_JS: &[u8] =
include_bytes!("../../front-end/playground_editor/mode-rust.js");
pub(crate) static THEME_DAWN_JS: &[u8] =
include_bytes!("../../front-end/playground_editor/theme-dawn.js");
pub(crate) static THEME_TOMORROW_NIGHT_JS: &[u8] =
include_bytes!("../../front-end/playground_editor/theme-tomorrow_night.js");

View file

@ -1,7 +0,0 @@
//! Theme dependencies for in-browser search. Not included in mdbook when
//! the "search" cargo feature is disabled.
pub(crate) static JS: &[u8] = include_bytes!("../../front-end/searcher/searcher.js");
pub(crate) static MARK_JS: &[u8] = include_bytes!("../../front-end/searcher/mark.min.js");
pub(crate) static ELASTICLUNR_JS: &[u8] =
include_bytes!("../../front-end/searcher/elasticlunr.min.js");

View file

@ -1,135 +0,0 @@
//! Utilities for processing HTML.
use std::collections::HashSet;
use std::path::{Component, Path, PathBuf};
/// Utility function to normalize path elements like `..`.
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(Component::RootDir);
}
Component::CurDir => {}
Component::ParentDir => {
if ret.ends_with(Component::ParentDir) {
ret.push(Component::ParentDir);
} else {
let popped = ret.pop();
if !popped && !ret.has_root() {
ret.push(Component::ParentDir);
}
}
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
/// Helper trait for converting a [`Path`] to a string suitable for an HTML path.
pub(crate) trait ToUrlPath {
fn to_url_path(&self) -> String;
}
impl ToUrlPath for Path {
fn to_url_path(&self) -> String {
// We're generally assuming that all paths we deal with are utf-8.
// The replace here is to handle Windows paths.
self.to_str().unwrap().replace('\\', "/")
}
}
/// Make sure an HTML id is unique.
///
/// Keeps a set of all previously returned IDs; if the requested id is already
/// used, numeric suffixes (-1, -2, ...) are tried until an unused one is found.
pub(crate) fn unique_id(id: &str, used: &mut HashSet<String>) -> String {
if used.insert(id.to_string()) {
return id.to_string();
}
// This ID is already in use. Generate one that is not by appending a
// numeric suffix.
let mut counter: u32 = 1;
loop {
let candidate = format!("{id}-{counter}");
if used.insert(candidate.clone()) {
return candidate;
}
counter += 1;
}
}
/// Generates an HTML id from the given text.
pub(crate) fn id_from_content(content: &str) -> String {
// This is intended to be close to how header ID generation is done in
// other sites and tools, but is not 100% the same. Not all sites and
// tools use the same algorithm. See these for more information:
//
// - https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links
// - https://docs.gitlab.com/user/markdown/#heading-ids-and-links
// - https://pandoc.org/MANUAL.html#extension-auto_identifiers
// - https://kramdown.gettalong.org/converter/html#auto-ids
// - https://docs.rs/comrak/latest/comrak/options/struct.Extension.html#structfield.header_ids
content
.trim()
.to_lowercase()
.chars()
.filter_map(|ch| {
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
Some(ch)
} else if ch.is_whitespace() {
Some('-')
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_generates_unique_ids() {
let mut id_counter = Default::default();
assert_eq!(unique_id("", &mut id_counter), "");
assert_eq!(unique_id("Über", &mut id_counter), "Über");
assert_eq!(unique_id("Über", &mut id_counter), "Über-1");
assert_eq!(unique_id("Über", &mut id_counter), "Über-2");
}
#[test]
fn it_normalizes_ids() {
assert_eq!(
id_from_content("`--passes`: add more rustdoc passes"),
"--passes-add-more-rustdoc-passes"
);
assert_eq!(
id_from_content("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-"
);
assert_eq!(id_from_content("_-_12345"), "_-_12345");
assert_eq!(id_from_content("12345"), "12345");
assert_eq!(id_from_content("中文"), "中文");
assert_eq!(id_from_content("にほんご"), "にほんご");
assert_eq!(id_from_content("한국어"), "한국어");
assert_eq!(id_from_content(""), "");
assert_eq!(id_from_content("中文標題 CJK title"), "中文標題-cjk-title");
assert_eq!(id_from_content("Über"), "über");
}
}

View file

@ -1,16 +0,0 @@
[package]
name = "mdbook-markdown"
version = "0.5.3"
description = "Markdown processing used in mdBook"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
pulldown-cmark.workspace = true
regex.workspace = true
tracing.workspace = true
[lints]
workspace = true

View file

@ -1,13 +0,0 @@
# mdbook-markdown
[![Documentation](https://img.shields.io/docsrs/mdbook-markdown)](https://docs.rs/mdbook-markdown)
[![crates.io](https://img.shields.io/crates/v/mdbook-markdown.svg)](https://crates.io/crates/mdbook-markdown)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the Markdown support library for [mdBook](https://rust-lang.github.io/mdBook/). Rust crates (such as preprocessors) can use this library to process Markdown in the same way as mdBook.
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -1,61 +0,0 @@
//! Markdown processing used in mdBook.
//!
//! This crate provides functions for processing Markdown in the same way as
//! [mdBook](https://rust-lang.github.io/mdBook/). The [`pulldown_cmark`]
//! crate is used as the underlying parser. This crate re-exports
//! [`pulldown_cmark`] so that you can access its types.
use pulldown_cmark::{Options, Parser};
#[doc(inline)]
pub use pulldown_cmark;
/// Options for parsing markdown.
#[non_exhaustive]
pub struct MarkdownOptions {
/// Enables smart punctuation.
///
/// Converts quotes to curly quotes, `...` to `…`, `--` to en-dash, and
/// `---` to em-dash.
///
/// This is `true` by default.
pub smart_punctuation: bool,
/// Enables definition lists.
///
/// This is `true` by default.
pub definition_lists: bool,
/// Enables admonitions.
///
/// This is `true` by default.
pub admonitions: bool,
}
impl Default for MarkdownOptions {
fn default() -> MarkdownOptions {
MarkdownOptions {
smart_punctuation: true,
definition_lists: true,
admonitions: true,
}
}
}
/// Creates a new pulldown-cmark parser of the given text.
pub fn new_cmark_parser<'text>(text: &'text str, options: &MarkdownOptions) -> Parser<'text> {
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 options.smart_punctuation {
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
}
if options.definition_lists {
opts.insert(Options::ENABLE_DEFINITION_LIST);
}
if options.admonitions {
opts.insert(Options::ENABLE_GFM);
}
Parser::new_ext(text, opts)
}

View file

@ -1,17 +0,0 @@
[package]
name = "mdbook-preprocessor"
version = "0.5.3"
description = "Library to assist implementing an mdBook preprocessor"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
mdbook-core.workspace = true
serde.workspace = true
serde_json.workspace = true
[lints]
workspace = true

View file

@ -1,13 +0,0 @@
# mdbook-preprocessor
[![Documentation](https://img.shields.io/docsrs/mdbook-preprocessor)](https://docs.rs/mdbook-preprocessor)
[![crates.io](https://img.shields.io/crates/v/mdbook-preprocessor.svg)](https://crates.io/crates/mdbook-preprocessor)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the Rust library to implement a [preprocessor](https://rust-lang.github.io/mdBook/for_developers/preprocessors.html) for [mdBook](https://rust-lang.github.io/mdBook/).
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -1,84 +0,0 @@
//! Library to assist implementing an mdbook preprocessor.
//!
//! This library is used to implement a
//! [preprocessor](https://rust-lang.github.io/mdBook/for_developers/preprocessors.html)
//! for [mdBook](https://rust-lang.github.io/mdBook/). See the linked chapter
//! for more information on how to implement a preprocessor.
use anyhow::Context;
use mdbook_core::book::Book;
use mdbook_core::config::Config;
use mdbook_core::errors::Result;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::Read;
use std::path::PathBuf;
pub use mdbook_core::MDBOOK_VERSION;
pub use mdbook_core::book;
pub use mdbook_core::config;
pub use mdbook_core::errors;
/// An operation which is run immediately after loading a book into memory and
/// before it gets rendered.
///
/// Types that implement the `Preprocessor` trait can be used with
/// [`MDBook::with_preprocessor`] to programmatically add preprocessors.
///
/// [`MDBook::with_preprocessor`]: https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html#method.with_preprocessor
pub trait Preprocessor {
/// Get the `Preprocessor`'s name.
fn name(&self) -> &str;
/// Run this `Preprocessor`, allowing it to update the book before it is
/// given to a renderer.
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
/// A hint to `MDBook` whether this preprocessor is compatible with a
/// particular renderer.
///
/// By default, always returns `true`.
fn supports_renderer(&self, _renderer: &str) -> Result<bool> {
Ok(true)
}
}
/// Extra information for a `Preprocessor` to give them more context when
/// processing a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PreprocessorContext {
/// The location of the book directory on disk.
pub root: PathBuf,
/// The book configuration (`book.toml`).
pub config: Config,
/// The `Renderer` this preprocessor is being used with.
pub renderer: String,
/// The calling `mdbook` version.
pub mdbook_version: String,
/// Internal mapping of chapter titles.
///
/// This is used internally by mdbook to compute custom chapter titles.
/// This should not be used outside of mdbook's internals.
#[serde(skip)]
pub chapter_titles: RefCell<HashMap<PathBuf, String>>,
}
impl PreprocessorContext {
/// Create a new `PreprocessorContext`.
pub fn new(root: PathBuf, config: Config, renderer: String) -> Self {
PreprocessorContext {
root,
config,
renderer,
mdbook_version: crate::MDBOOK_VERSION.to_string(),
chapter_titles: RefCell::new(HashMap::new()),
}
}
}
/// Parses the input given to a preprocessor.
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
}

View file

@ -1,17 +0,0 @@
[package]
name = "mdbook-renderer"
version = "0.5.3"
description = "Library to assist implementing an mdBook renderer"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
mdbook-core.workspace = true
serde.workspace = true
serde_json.workspace = true
[lints]
workspace = true

View file

@ -1,13 +0,0 @@
# mdbook-renderer
[![Documentation](https://img.shields.io/docsrs/mdbook-renderer)](https://docs.rs/mdbook-renderer)
[![crates.io](https://img.shields.io/crates/v/mdbook-renderer.svg)](https://crates.io/crates/mdbook-renderer)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the Rust library to implement a [renderer](https://rust-lang.github.io/mdBook/for_developers/backends.html) for [mdBook](https://rust-lang.github.io/mdBook/).
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -1,89 +0,0 @@
//! Library to assist implementing an mdbook renderer.
//!
//! This library is used to implement a
//! [renderer](https://rust-lang.github.io/mdBook/for_developers/backends.html)
//! for [mdBook](https://rust-lang.github.io/mdBook/). See the linked chapter
//! for more information on how to implement a renderer.
use anyhow::Context;
use mdbook_core::book::Book;
use mdbook_core::config::Config;
use mdbook_core::errors::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Read;
use std::path::PathBuf;
pub use mdbook_core::MDBOOK_VERSION;
pub use mdbook_core::book;
pub use mdbook_core::config;
pub use mdbook_core::errors;
/// An mdbook backend.
///
/// Types that implement the `Renderer` trait can be used with
/// [`MDBook::with_renderer`] to programmatically add renderers.
///
/// [`MDBook::with_renderer`]: https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html#method.with_renderer
pub trait Renderer {
/// The `Renderer`'s name.
fn name(&self) -> &str;
/// Invoke the `Renderer`, passing in all the necessary information for
/// describing a book.
fn render(&self, ctx: &RenderContext) -> Result<()>;
}
/// The context provided to all renderers.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RenderContext {
/// Which version of `mdbook` did this come from (as written in `mdbook`'s
/// `Cargo.toml`). Useful if you know the renderer is only compatible with
/// certain versions of `mdbook`.
pub version: String,
/// The book's root directory.
pub root: PathBuf,
/// A loaded representation of the book itself.
pub book: Book,
/// The loaded configuration file.
pub config: Config,
/// Where the renderer *must* put any build artefacts generated. To allow
/// renderers to cache intermediate results, this directory is not
/// guaranteed to be empty or even exist.
pub destination: PathBuf,
/// Internal mapping of chapter titles.
///
/// This is used internally by mdbook to compute custom chapter titles.
/// This should not be used outside of mdbook's internals.
#[serde(skip)]
pub chapter_titles: HashMap<PathBuf, String>,
}
impl RenderContext {
/// Create a new `RenderContext`.
pub fn new<P, Q>(root: P, book: Book, config: Config, destination: Q) -> RenderContext
where
P: Into<PathBuf>,
Q: Into<PathBuf>,
{
RenderContext {
book,
config,
version: crate::MDBOOK_VERSION.to_string(),
root: root.into(),
destination: destination.into(),
chapter_titles: HashMap::new(),
}
}
/// Get the source directory's (absolute) path on disk.
pub fn source_dir(&self) -> PathBuf {
self.root.join(&self.config.book.src)
}
/// Load a `RenderContext` from its JSON representation.
pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> {
serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`")
}
}

View file

@ -1,19 +0,0 @@
[package]
name = "mdbook-summary"
version = "0.5.3"
description = "Summary parser for mdBook"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
mdbook-core.workspace = true
memchr.workspace = true
pulldown-cmark.workspace = true
serde.workspace = true
tracing.workspace = true
[lints]
workspace = true

View file

@ -1,13 +0,0 @@
# mdbook-summary
[![Documentation](https://img.shields.io/docsrs/mdbook-summary)](https://docs.rs/mdbook-summary)
[![crates.io](https://img.shields.io/crates/v/mdbook-summary.svg)](https://crates.io/crates/mdbook-summary)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the Rust library used to parse the [`SUMMARY.md`](https://rust-lang.github.io/mdBook/format/summary.html) file structure for [mdBook](https://rust-lang.github.io/mdBook/).
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -1,12 +0,0 @@
[package]
name = "xtask"
publish = false
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
[lints]
workspace = true

View file

@ -1,4 +0,0 @@
# xtask
This is a CLI utility for running development commands for mdbook.
See [CONTRIBUTING.md](../../CONTRIBUTING.md) for how to use this.

View file

@ -1,120 +0,0 @@
//! Helper to generate a changelog for a new release.
use super::Result;
use std::fs;
use std::process::Command;
use std::process::exit;
const CHANGELOG_PATH: &str = "CHANGELOG.md";
pub(crate) fn changelog() -> Result<()> {
let previous = get_previous()?;
let current = get_current()?;
if current == previous {
eprintln!(
"error: Current version is `{current}` which is the same as the \
previous version in the changelog. Run `cargo set-version --bump <BUMP> first."
);
exit(1);
}
let prs = get_prs(&previous)?;
update_changelog(&previous, &current, &prs)?;
Ok(())
}
fn get_previous() -> Result<String> {
let contents = fs::read_to_string(CHANGELOG_PATH)?;
let version = contents
.lines()
.filter_map(|line| line.strip_prefix("## mdBook "))
.next()
.expect("at least one entry")
.to_owned();
Ok(version)
}
fn get_current() -> Result<String> {
let contents = fs::read_to_string("Cargo.toml")?;
let mut lines = contents
.lines()
.filter_map(|line| line.strip_prefix("version = "))
.map(|version| &version[1..version.len() - 1]);
let version = lines.next().expect("version should exist").to_owned();
assert_eq!(lines.next(), None);
Ok(version)
}
fn get_prs(previous: &str) -> Result<Vec<(String, String)>> {
println!("running `git fetch upstream`");
let status = Command::new("git").args(["fetch", "upstream"]).status()?;
if !status.success() {
eprintln!("error: git fetch failed");
exit(1);
}
println!("running `git log`");
const SEPARATOR: &str = "---COMMIT_SEPARATOR---";
let output = Command::new("git")
.args([
"log",
"--first-parent",
&format!("--pretty=format:%B%n{SEPARATOR}"),
"upstream/master",
&format!("v{previous}...upstream/HEAD"),
])
.output()?;
if !output.status.success() {
eprintln!("error: git log failed");
exit(1);
}
let stdout = std::str::from_utf8(&output.stdout).unwrap();
let prs = stdout
.split(&format!("{SEPARATOR}\n"))
.filter_map(|entry| {
let mut lines = entry.lines();
let first = match lines.next().unwrap().strip_prefix("Merge pull request #") {
Some(f) => f,
None => {
println!("warning: merge line not found in {entry}");
return None;
}
};
let number = first.split_whitespace().next().unwrap();
assert_eq!(lines.next(), Some(""));
let title = lines.next().expect("title is set");
assert_eq!(lines.next(), Some(""));
Some((number.to_string(), title.to_string()))
})
.collect();
Ok(prs)
}
fn update_changelog(previous: &str, current: &str, prs: &[(String, String)]) -> Result<()> {
let prs: String = prs
.iter()
.map(|(number, title)| {
format!(
"- {title}\n \
[#{number}](https://github.com/rust-lang/mdBook/pull/{number})\n"
)
})
.collect();
let new = format!(
"## mdBook {current}\n\
[v{previous}...v{current}](https://github.com/rust-lang/mdBook/compare/v{previous}...v{current})\n\
\n\
{prs}\
\n\
### Added\n\
\n\
### Changed\n\
\n\
### Fixed\n\
\n"
);
let mut contents = fs::read_to_string(CHANGELOG_PATH)?;
let insertion_point = contents.find("## ").unwrap();
contents.insert_str(insertion_point, &new);
fs::write(CHANGELOG_PATH, contents)?;
Ok(())
}

View file

@ -1,165 +0,0 @@
//! Helper for local development.
use std::collections::BTreeMap;
use std::error::Error;
use std::io::Write;
use std::process::exit;
use std::process::{Command, Stdio};
mod changelog;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
macro_rules! commands {
($($name:literal => $func:expr),* $(,)?) => {
[$(($name, $func as fn() -> Result<()>)),*]
};
}
let cmds: BTreeMap<&'static str, fn() -> Result<()>> = commands! {
"test-all" => test_all,
"test-workspace" => test_workspace,
"clippy" => clippy,
"doc" => doc,
"fmt" => fmt,
"semver-checks" => semver_checks,
"eslint" => eslint,
"gui" => gui,
"changelog" => changelog::changelog,
}
.into_iter()
.collect();
let keys = cmds.keys().copied().collect::<Vec<_>>().join(", ");
let mut args = std::env::args().skip(1).peekable();
if args.peek().is_none() {
eprintln!("error: specify a command (valid options: {keys})");
exit(1);
}
while let Some(arg) = args.next() {
if let Some(cmd_fn) = cmds.get(arg.as_str()) {
cmd_fn()?;
} else if arg == "bump" {
let bump_arg = args
.next()
.expect("the next argument should be one of major, minor, patch, rc, beta, alpha");
bump(&bump_arg)?;
} else if matches!(arg.as_str(), "-h" | "--help") {
println!("valid options: {keys}");
exit(0)
} else {
eprintln!("error: unknown command `{arg}` (valid options: {keys}");
exit(1);
}
}
println!("success!");
Ok(())
}
fn test_all() -> Result<()> {
test_workspace()?;
clippy()?;
doc()?;
fmt()?;
semver_checks()?;
eslint()?;
gui()?;
Ok(())
}
fn cargo(args: &str, cb: &dyn Fn(&mut Command)) -> Result<()> {
println!("Running `cargo {args}`");
let mut cmd = Command::new("cargo");
cmd.args(args.split_whitespace());
cb(&mut cmd);
let status = cmd.status().expect("cargo should be installed");
if !status.success() {
return Err(format!("command `cargo {args}` failed").into());
}
Ok(())
}
fn test_workspace() -> Result<()> {
cargo("test --workspace", &|_| {})?;
cargo("test --workspace --no-default-features", &|_| {})?;
Ok(())
}
fn clippy() -> Result<()> {
cargo(
"clippy --workspace --all-targets --no-deps -- -D warnings",
&|_| {},
)?;
Ok(())
}
fn doc() -> Result<()> {
cargo(
"doc --workspace --document-private-items --no-deps",
&|cmd| {
cmd.env("RUSTDOCFLAGS", "-D warnings");
},
)?;
Ok(())
}
fn fmt() -> Result<()> {
cargo("fmt --check", &|_| {})?;
Ok(())
}
fn semver_checks() -> Result<()> {
cargo("+stable semver-checks --workspace", &|_| {})?;
Ok(())
}
fn gui() -> Result<()> {
cargo("test --test gui", &|_| {})?;
Ok(())
}
fn eslint() -> Result<()> {
println!("Running `npm run lint`");
let status = Command::new("npm")
.args(["run", "lint"])
.status()
.expect("npm should be installed");
if !status.success() {
return Err("eslint failed".into());
}
Ok(())
}
fn bump(bump: &str) -> Result<()> {
// Grab all the publishable crate names.
let metadata = Command::new("cargo")
.args(["metadata", "--format-version=1", "--no-deps"])
.output()?;
let mut jq = Command::new("jq")
.args(["-r", ".packages[] | select(.publish == null) | .name"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
jq.stdin.as_mut().unwrap().write_all(&metadata.stdout)?;
let jq_out = jq.wait_with_output()?;
if !jq_out.status.success() {
eprintln!("jq failed");
exit(1);
}
let names = std::str::from_utf8(&jq_out.stdout).unwrap();
let mut names: Vec<_> = names.split_whitespace().collect();
for i in (0..names.len()).rev() {
names.insert(i, "-p");
}
let status = Command::new("cargo")
.args(["set-version", "--bump"])
.arg(bump)
.args(names)
.status()?;
if !status.success() {
eprintln!("cargo set-version failed");
exit(1);
}
Ok(())
}

View file

@ -1,98 +0,0 @@
import { defineConfig, globalIgnores } from "eslint/config";
// Custom preprocessor to strip Handlebars templates.
const handlebarsPreprocessor = {
processors: {
"handlebars-js": {
preprocess(text, filename) {
if (filename.endsWith('.hbs')) {
// This is a really dumb strip, which will likely not work
// for more complex expressions, but for our use is good
// enough for now.
return [text.replace(/\{\{.*?\}\}/g, '')];
}
return [text];
},
postprocess(messages, filename) {
// Ideally this would update the locations so that they would
// compensate for the removed ranges.
return [].concat(...messages);
},
},
},
};
export default defineConfig([
globalIgnores(["**/**min.js", "**/highlight.js", "**/playground_editor/*"]),
{
rules: {
indent: ["error", 4],
"linebreak-style": ["error", "unix"],
quotes: ["error", "single"],
semi: ["error", "always"],
"brace-style": ["error", "1tbs", {
allowSingleLine: false,
}],
curly: "error",
"no-trailing-spaces": "error",
"no-multi-spaces": "error",
"keyword-spacing": ["error", {
before: true,
after: true,
}],
"comma-spacing": ["error", {
before: false,
after: true,
}],
"arrow-spacing": ["error", {
before: true,
after: true,
}],
"key-spacing": ["error", {
beforeColon: false,
afterColon: true,
mode: "strict",
}],
"func-call-spacing": ["error", "never"],
"space-infix-ops": "error",
"space-before-function-paren": ["error", "never"],
"space-before-blocks": "error",
"no-console": ["error", {
allow: ["warn", "error"],
}],
"comma-dangle": ["error", "always-multiline"],
"comma-style": ["error", "last"],
"max-len": ["error", {
code: 100,
tabWidth: 2,
}],
"eol-last": ["error", "always"],
"no-extra-parens": "error",
"arrow-parens": ["error", "as-needed"],
"no-unused-vars": ["error", {
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
}],
"prefer-const": ["error"],
"no-var": "error",
eqeqeq: "error",
},
},
{
files: ["**/*.js.hbs"],
processor: handlebarsPreprocessor.processors["handlebars-js"],
},
]);

View file

@ -1,15 +1,13 @@
//! A basic example of a preprocessor that does nothing.
use crate::nop_lib::Nop; use crate::nop_lib::Nop;
use clap::{Arg, ArgMatches, Command}; use clap::{Arg, ArgMatches, Command};
use mdbook_preprocessor::book::Book; use mdbook::book::Book;
use mdbook_preprocessor::errors::Result; use mdbook::errors::Error;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
use semver::{Version, VersionReq}; use semver::{Version, VersionReq};
use std::io; use std::io;
use std::process; use std::process;
fn make_app() -> Command { pub fn make_app() -> Command {
Command::new("nop-preprocessor") Command::new("nop-preprocessor")
.about("A mdbook preprocessor which does precisely nothing") .about("A mdbook preprocessor which does precisely nothing")
.subcommand( .subcommand(
@ -28,23 +26,23 @@ fn main() {
if let Some(sub_args) = matches.subcommand_matches("supports") { if let Some(sub_args) = matches.subcommand_matches("supports") {
handle_supports(&preprocessor, sub_args); handle_supports(&preprocessor, sub_args);
} else if let Err(e) = handle_preprocessing(&preprocessor) { } else if let Err(e) = handle_preprocessing(&preprocessor) {
eprintln!("{e:?}"); eprintln!("{e}");
process::exit(1); process::exit(1);
} }
} }
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<()> { fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
let (ctx, book) = mdbook_preprocessor::parse_input(io::stdin())?; let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
let book_version = Version::parse(&ctx.mdbook_version)?; let book_version = Version::parse(&ctx.mdbook_version)?;
let version_req = VersionReq::parse(mdbook_preprocessor::MDBOOK_VERSION)?; let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
if !version_req.matches(&book_version) { if !version_req.matches(&book_version) {
eprintln!( eprintln!(
"Warning: The {} plugin was built against version {} of mdbook, \ "Warning: The {} plugin was built against version {} of mdbook, \
but we're being called from version {}", but we're being called from version {}",
pre.name(), pre.name(),
mdbook_preprocessor::MDBOOK_VERSION, mdbook::MDBOOK_VERSION,
ctx.mdbook_version ctx.mdbook_version
); );
} }
@ -59,7 +57,7 @@ fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
let renderer = sub_args let renderer = sub_args
.get_one::<String>("renderer") .get_one::<String>("renderer")
.expect("Required argument"); .expect("Required argument");
let supported = pre.supports_renderer(renderer).unwrap(); let supported = pre.supports_renderer(renderer);
// Signal whether the renderer is supported by exiting with 1 or 0. // Signal whether the renderer is supported by exiting with 1 or 0.
if supported { if supported {
@ -71,7 +69,6 @@ fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
/// The actual implementation of the `Nop` preprocessor. This would usually go /// The actual implementation of the `Nop` preprocessor. This would usually go
/// in your main `lib.rs` file. /// in your main `lib.rs` file.
#[allow(unreachable_pub, reason = "wouldn't be a problem in a proper lib.rs")]
mod nop_lib { mod nop_lib {
use super::*; use super::*;
@ -89,24 +86,21 @@ mod nop_lib {
"nop-preprocessor" "nop-preprocessor"
} }
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> { fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book, Error> {
// In testing we want to tell the preprocessor to blow up by setting a // In testing we want to tell the preprocessor to blow up by setting a
// particular config value // particular config value
match ctx if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
.config if nop_cfg.contains_key("blow-up") {
.get::<bool>("preprocessor.nop-preprocessor.blow-up") anyhow::bail!("Boom!!1!");
{ }
Ok(Some(true)) => anyhow::bail!("Boom!!1!"),
Ok(_) => {}
Err(e) => anyhow::bail!("expect bool for blow-up: {e}"),
} }
// we *are* a no-op preprocessor after all // we *are* a no-op preprocessor after all
Ok(book) Ok(book)
} }
fn supports_renderer(&self, renderer: &str) -> Result<bool> { fn supports_renderer(&self, renderer: &str) -> bool {
Ok(renderer != "not-supported") renderer != "not-supported"
} }
} }
@ -123,6 +117,7 @@ mod nop_lib {
"book": { "book": {
"authors": ["AUTHOR"], "authors": ["AUTHOR"],
"language": "en", "language": "en",
"multilingual": false,
"src": "src", "src": "src",
"title": "TITLE" "title": "TITLE"
}, },
@ -134,7 +129,7 @@ mod nop_lib {
"mdbook_version": "0.4.21" "mdbook_version": "0.4.21"
}, },
{ {
"items": [ "sections": [
{ {
"Chapter": { "Chapter": {
"name": "Chapter 1", "name": "Chapter 1",
@ -146,12 +141,13 @@ mod nop_lib {
"parent_names": [] "parent_names": []
} }
} }
] ],
"__non_exhaustive": null
} }
]"##; ]"##;
let input_json = input_json.as_bytes(); let input_json = input_json.as_bytes();
let (ctx, book) = mdbook_preprocessor::parse_input(input_json).unwrap(); let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
let expected_book = book.clone(); let expected_book = book.clone();
let result = Nop::new().run(&ctx, book); let result = Nop::new().run(&ctx, book);
assert!(result.is_ok()); assert!(result.is_ok());

View file

@ -1 +0,0 @@
book

View file

@ -1,5 +0,0 @@
[book]
title = "remove-emphasis"
[preprocessor.remove-emphasis]
command = "cargo run --manifest-path=mdbook-remove-emphasis/Cargo.toml --locked"

View file

@ -1,16 +0,0 @@
[package]
name = "mdbook-remove-emphasis"
version = "0.1.0"
edition.workspace = true
publish = false
[dependencies]
mdbook-preprocessor.workspace = true
pulldown-cmark = { workspace = true, default-features = false }
pulldown-cmark-to-cmark = "22.0.0"
serde_json.workspace = true
[[bin]]
name = "mdbook-remove-emphasis"
# This is tested through a separate test from the main package.
test = false

View file

@ -1,73 +0,0 @@
//! This is a demonstration of an mdBook preprocessor which parses markdown
//! and removes any instances of emphasis.
use mdbook_preprocessor::book::{Book, Chapter};
use mdbook_preprocessor::errors::Result;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use pulldown_cmark::{Event, Parser, Tag, TagEnd};
use std::io;
fn main() {
let mut args = std::env::args().skip(1);
match args.next().as_deref() {
Some("supports") => {
// Supports all renderers.
return;
}
Some(arg) => {
eprintln!("unknown argument: {arg}");
std::process::exit(1);
}
None => {}
}
if let Err(e) = handle_preprocessing() {
eprintln!("{e}");
std::process::exit(1);
}
}
struct RemoveEmphasis;
impl Preprocessor for RemoveEmphasis {
fn name(&self) -> &str {
"remove-emphasis"
}
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
let mut total = 0;
book.for_each_chapter_mut(|ch| match remove_emphasis(&mut total, ch) {
Ok(s) => ch.content = s,
Err(e) => eprintln!("failed to process chapter: {e:?}"),
});
eprintln!("removed {total} emphasis");
Ok(book)
}
}
// ANCHOR: remove_emphasis
fn remove_emphasis(num_removed_items: &mut usize, chapter: &mut Chapter) -> Result<String> {
let mut buf = String::with_capacity(chapter.content.len());
let events = Parser::new(&chapter.content).filter(|e| match e {
Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong) => {
*num_removed_items += 1;
false
}
Event::End(TagEnd::Emphasis) | Event::End(TagEnd::Strong) => false,
_ => true,
});
Ok(pulldown_cmark_to_cmark::cmark(events, &mut buf).map(|_| buf)?)
}
// ANCHOR_END: remove_emphasis
pub fn handle_preprocessing() -> Result<()> {
let pre = RemoveEmphasis;
let (ctx, book) = mdbook_preprocessor::parse_input(io::stdin())?;
let processed_book = pre.run(&ctx, book)?;
serde_json::to_writer(io::stdout(), &processed_book)?;
Ok(())
}

View file

@ -1,3 +0,0 @@
# Summary
- [Chapter 1](./chapter_1.md)

View file

@ -1,3 +0,0 @@
# Chapter 1
This has *light emphasis* and **bold emphasis**.

View file

@ -1,13 +0,0 @@
//! A test to ensure that the remove-emphasis example works.
#[test]
fn remove_emphasis_works() {
// Tests that the remove-emphasis example works as expected.
// Workaround for https://github.com/rust-lang/mdBook/issues/1424
std::env::set_current_dir("examples/remove-emphasis").unwrap();
let book = mdbook_driver::MDBook::load(".").unwrap();
book.build().unwrap();
let ch1 = std::fs::read_to_string("book/chapter_1.html").unwrap();
assert!(ch1.contains("This has light emphasis and bold emphasis."));
}

View file

@ -13,7 +13,6 @@ mathjax-support = true
site-url = "/mdBook/" site-url = "/mdBook/"
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide" git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}" edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
hash-files = true
[output.html.playground] [output.html.playground]
editable = true editable = true
@ -33,9 +32,3 @@ heading-split-level = 2
[output.html.redirect] [output.html.redirect]
"/format/config.html" = "configuration/index.html" "/format/config.html" = "configuration/index.html"
[preprocessor.guide-helper]
command = "cargo run --quiet --manifest-path guide-helper/Cargo.toml"
[build]
extra-watch-dirs = ["guide-helper/src"]

View file

@ -1,16 +0,0 @@
[package]
name = "guide-helper"
publish = false
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
mdbook-preprocessor.workspace = true
semver.workspace = true
serde_json.workspace = true
toml.workspace = true
[lints]
workspace = true

View file

@ -1,61 +0,0 @@
//! Preprocessor for the mdBook guide.
use mdbook_preprocessor::book::Book;
use mdbook_preprocessor::errors::Result;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use semver::{Version, VersionReq};
use std::io;
/// Preprocessing entry point.
pub fn handle_preprocessing() -> Result<()> {
let pre = GuideHelper;
let (ctx, book) = mdbook_preprocessor::parse_input(io::stdin())?;
let book_version = Version::parse(&ctx.mdbook_version)?;
let version_req = VersionReq::parse(mdbook_preprocessor::MDBOOK_VERSION)?;
if !version_req.matches(&book_version) {
eprintln!(
"warning: The {} plugin was built against version {} of mdbook, \
but we're being called from version {}",
pre.name(),
mdbook_preprocessor::MDBOOK_VERSION,
ctx.mdbook_version
);
}
let processed_book = pre.run(&ctx, book)?;
serde_json::to_writer(io::stdout(), &processed_book)?;
Ok(())
}
struct GuideHelper;
impl Preprocessor for GuideHelper {
fn name(&self) -> &str {
"guide-helper"
}
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
insert_version(&mut book);
Ok(book)
}
}
fn insert_version(book: &mut Book) {
let path = std::env::current_dir()
.unwrap()
.parent()
.unwrap()
.join("Cargo.toml");
let manifest_contents = std::fs::read_to_string(&path).unwrap();
let manifest: toml::Value = toml::from_str(&manifest_contents).unwrap();
let version = manifest["package"]["version"].as_str().unwrap();
const MARKER: &str = "{{ mdbook-version }}";
book.for_each_chapter_mut(|ch| {
if ch.content.contains(MARKER) {
ch.content = ch.content.replace(MARKER, version);
}
});
}

View file

@ -1,21 +0,0 @@
//! Preprocessor for the mdBook guide.
fn main() {
let mut args = std::env::args().skip(1);
match args.next().as_deref() {
Some("supports") => {
// Supports all renderers.
return;
}
Some(arg) => {
eprintln!("unknown argument: {arg}");
std::process::exit(1);
}
None => {}
}
if let Err(e) = guide_helper::handle_preprocessing() {
eprintln!("{e:?}");
std::process::exit(1);
}
}

View file

@ -1,22 +1,5 @@
# Introduction # Introduction
<style>
.mdbook-version {
position: absolute;
right: 20px;
top: 60px;
background-color: var(--theme-popup-bg);
border-radius: 8px;
padding: 2px 5px 2px 5px;
border: 1px solid var(--theme-popup-border);
font-size: 0.9em;
}
</style>
<div class="mdbook-version">
Version: {{ mdbook-version }}
</div>
**mdBook** is a command line tool to create books with Markdown. **mdBook** is a command line tool to create books with Markdown.
It is ideal for creating product or API documentation, tutorials, course materials or anything that requires a clean, It is ideal for creating product or API documentation, tutorials, course materials or anything that requires a clean,
easily navigable and customizable presentation. easily navigable and customizable presentation.

View file

@ -2,15 +2,15 @@
[Introduction](README.md) [Introduction](README.md)
# User guide # User Guide
- [Installation](guide/installation.md) - [Installation](guide/installation.md)
- [Reading books](guide/reading.md) - [Reading Books](guide/reading.md)
- [Creating a book](guide/creating.md) - [Creating a Book](guide/creating.md)
# Reference guide # Reference Guide
- [Command-line tool](cli/README.md) - [Command Line Tool](cli/README.md)
- [init](cli/init.md) - [init](cli/init.md)
- [build](cli/build.md) - [build](cli/build.md)
- [watch](cli/watch.md) - [watch](cli/watch.md)
@ -25,18 +25,18 @@
- [General](format/configuration/general.md) - [General](format/configuration/general.md)
- [Preprocessors](format/configuration/preprocessors.md) - [Preprocessors](format/configuration/preprocessors.md)
- [Renderers](format/configuration/renderers.md) - [Renderers](format/configuration/renderers.md)
- [Environment variables](format/configuration/environment-variables.md) - [Environment Variables](format/configuration/environment-variables.md)
- [Theme](format/theme/README.md) - [Theme](format/theme/README.md)
- [index.hbs](format/theme/index-hbs.md) - [index.hbs](format/theme/index-hbs.md)
- [Syntax highlighting](format/theme/syntax-highlighting.md) - [Syntax highlighting](format/theme/syntax-highlighting.md)
- [Editor](format/theme/editor.md) - [Editor](format/theme/editor.md)
- [MathJax support](format/mathjax.md) - [MathJax Support](format/mathjax.md)
- [mdBook-specific features](format/mdbook.md) - [mdBook-specific features](format/mdbook.md)
- [Markdown](format/markdown.md) - [Markdown](format/markdown.md)
- [Continuous integration](continuous-integration.md) - [Continuous Integration](continuous-integration.md)
- [For developers](for_developers/README.md) - [For Developers](for_developers/README.md)
- [Preprocessors](for_developers/preprocessors.md) - [Preprocessors](for_developers/preprocessors.md)
- [Alternative backends](for_developers/backends.md) - [Alternative Backends](for_developers/backends.md)
----------- -----------

View file

@ -1,4 +1,4 @@
# Command-line tool # Command Line Tool
The `mdbook` command-line tool is used to create and build books. The `mdbook` command-line tool is used to create and build books.
After you have [installed](../guide/installation.md) `mdbook`, you can run the `mdbook help` command in your terminal to view the available commands. After you have [installed](../guide/installation.md) `mdbook`, you can run the `mdbook help` command in your terminal to view the available commands.

View file

@ -30,7 +30,7 @@ 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 The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. Relative paths are interpreted relative to the current directory. If 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 not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`. `book.toml`, or to `./book`.

View file

@ -20,7 +20,7 @@ mdbook clean path/to/book
The `--dest-dir` (`-d`) option allows you to override the book's output 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 directory, which will be deleted by this command. Relative paths are interpreted
relative to the current directory. If not specified it will default to the 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`. value of the `build.build-dir` key in `book.toml`, or to `./book`.
```bash ```bash

View file

@ -40,20 +40,10 @@ 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 The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. Relative paths are interpreted relative to the current directory. If 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 not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`. `book.toml`, or to `./book`.
#### `--preserve-site-url`
By default `serve` overrides the [`output.html.site-url`] setting to `/`, since
the book is hosted at the root of the local server and links must resolve there.
When you have configured a `site-url` (for example to emit absolute links for a
book hosted in a subdirectory), the `--preserve-site-url` flag keeps the
configured value so you can preview those production links locally.
[`output.html.site-url`]: ../format/configuration/renderers.md#html-renderer-options
{{#include arg-watcher.md}} {{#include arg-watcher.md}}
#### Specify exclude patterns #### Specify exclude patterns

View file

@ -54,6 +54,13 @@ 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) 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. for more information.
#### `--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 The `--chapter` (`-c`) option allows you to test a specific chapter of the

View file

@ -23,7 +23,7 @@ your default web browser.
#### `--dest-dir` #### `--dest-dir`
The `--dest-dir` (`-d`) option allows you to change the output directory for the The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. Relative paths are interpreted relative to the current directory. If 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 not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`. `book.toml`, or to `./book`.

View file

@ -1,4 +1,4 @@
# Running `mdbook` in continuous integration # Running `mdbook` in Continuous Integration
There are a variety of services such as [GitHub Actions] or [GitLab CI/CD] which can be used to test and deploy your book automatically. There are a variety of services such as [GitHub Actions] or [GitLab CI/CD] which can be used to test and deploy your book automatically.
@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex
```sh ```sh
mkdir bin mkdir bin
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v{{ mdbook-version }}/mdbook-v{{ mdbook-version }}-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 bin/mdbook build
``` ```
@ -74,11 +74,11 @@ Some services have Rust pre-installed, but if your service does not, you will ne
Other than making sure the appropriate version of Rust is installed, there's not much more than just running `mdbook test` from the book directory. Other than making sure the appropriate version of Rust is installed, there's not much more than just running `mdbook test` from the book directory.
You may also want to consider running other kinds of tests, like [mdbook-linkcheck2] which will check for broken links. You may also want to consider running other kinds of tests, like [mdbook-linkcheck] which will check for broken links.
Or if you have your own style checks, spell checker, or any other tests it might be good to run them in CI. Or if you have your own style checks, spell checker, or any other tests it might be good to run them in CI.
[`mdbook test`]: cli/test.md [`mdbook test`]: cli/test.md
[mdbook-linkcheck2]: https://github.com/marxin/mdbook-linkcheck2#continuous-integration [mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck#continuous-integration
## Deploying ## Deploying

View file

@ -1,7 +1,7 @@
# For developers # For Developers
While `mdbook` is mainly used as a command line tool, you can also import the While `mdbook` is mainly used as a command line tool, you can also import the
underlying libraries directly and use those to manage a book. It also has a fairly underlying library directly and use that to manage a book. It also has a fairly
flexible plugin mechanism, allowing you to create your own custom tooling and flexible plugin mechanism, allowing you to create your own custom tooling and
consumers (often referred to as *backends*) if you need to do some analysis of consumers (often referred to as *backends*) if you need to do some analysis of
the book or render it in a different format. the book or render it in a different format.
@ -14,7 +14,8 @@ The two main ways a developer can hook into the book's build process is via,
- [Preprocessors](preprocessors.md) - [Preprocessors](preprocessors.md)
- [Alternative Backends](backends.md) - [Alternative Backends](backends.md)
## The build process
## The Build Process
The process of rendering a book project goes through several steps. The process of rendering a book project goes through several steps.
@ -27,18 +28,20 @@ The process of rendering a book project goes through several steps.
1. Run all the preprocessors. 1. Run all the preprocessors.
2. Call the backend to render the processed result. 2. Call the backend to render the processed result.
## Using `mdbook` as a library
The `mdbook` binary is just a wrapper around the underlying mdBook crates, ## Using `mdbook` as a Library
exposing their functionality as a command-line program. If you want to
programmatically drive mdBook, you can use the [`mdbook-driver`] crate.
This can be used to add your own functionality or tweak the build process.
The easiest way to find out how to use the `mdbook-driver` crate is by looking at the The `mdbook` binary is just a wrapper around the `mdbook` crate, exposing its
functionality as a command-line program. As such it is quite easy to create your
own programs which use `mdbook` internally, adding your own functionality (e.g.
a custom preprocessor) or tweaking the build process.
The easiest way to find out how to use the `mdbook` crate is by looking at the
[API Docs]. The top level documentation explains how one would use the [API Docs]. The top level documentation explains how one would use the
[`MDBook`] type to load and build a book, while the [config] module gives a good [`MDBook`] type to load and build a book, while the [config] module gives a good
explanation on the configuration system. explanation on the configuration system.
[`MDBook`]: https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html
[API Docs]: https://docs.rs/mdbook-driver/latest/mdbook_driver/ [`MDBook`]: https://docs.rs/mdbook/*/mdbook/book/struct.MDBook.html
[config]: https://docs.rs/mdbook-driver/latest/mdbook_driver/config/index.html [API Docs]: https://docs.rs/mdbook/*/mdbook/
[config]: https://docs.rs/mdbook/*/mdbook/config/index.html

View file

@ -1,4 +1,4 @@
# Alternative backends # Alternative Backends
A "backend" is simply a program which `mdbook` will invoke during the book A "backend" is simply a program which `mdbook` will invoke during the book
rendering process. This program is passed a JSON representation of the book and rendering process. This program is passed a JSON representation of the book and
@ -10,19 +10,19 @@ See [Configuring Renderers](../format/configuration/renderers.md) for more infor
The community has developed several backends. The community has developed several backends.
See the [Third Party Plugins] wiki page for a list of available backends. See the [Third Party Plugins] wiki page for a list of available backends.
## Setting up ## Setting Up
This page will step you through creating your own alternative backend in the form This page will step you through creating your own alternative backend in the form
of a simple word counting program. Although it will be written in Rust, there's of a simple word counting program. Although it will be written in Rust, there's
no reason why it couldn't be accomplished using something like Python or Ruby. no reason why it couldn't be accomplished using something like Python or Ruby.
First you'll want to create a new binary program and add `mdbook-renderer` as a First you'll want to create a new binary program and add `mdbook` as a
dependency. dependency.
```shell ```shell
$ cargo new --bin mdbook-wordcount $ cargo new --bin mdbook-wordcount
$ cd mdbook-wordcount $ cd mdbook-wordcount
$ cargo add mdbook-renderer $ cargo add mdbook
``` ```
When our `mdbook-wordcount` plugin is invoked, `mdbook` will send it a JSON When our `mdbook-wordcount` plugin is invoked, `mdbook` will send it a JSON
@ -33,8 +33,10 @@ This is all the boilerplate necessary for our backend to load the book.
```rust ```rust
// src/main.rs // src/main.rs
extern crate mdbook;
use std::io; use std::io;
use mdbook_renderer::RenderContext; use mdbook::renderer::RenderContext;
fn main() { fn main() {
let mut stdin = io::stdin(); let mut stdin = io::stdin();
@ -43,14 +45,15 @@ fn main() {
``` ```
> **Note:** The `RenderContext` contains a `version` field. This lets backends > **Note:** The `RenderContext` contains a `version` field. This lets backends
> figure out whether they are compatible with the version of `mdbook` it's being figure out whether they are compatible with the version of `mdbook` it's being
> called by. This `version` comes directly from the corresponding field in called by. This `version` comes directly from the corresponding field in
> `mdbook`'s `Cargo.toml`. `mdbook`'s `Cargo.toml`.
>
> It is recommended that backends use the [`semver`] crate to inspect this field
> and emit a warning if there may be a compatibility issue.
## Inspecting the book It is recommended that backends use the [`semver`] crate to inspect this field
and emit a warning if there may be a compatibility issue.
## Inspecting the Book
Now our backend has a copy of the book, lets count how many words are in each Now our backend has a copy of the book, lets count how many words are in each
chapter! chapter!
@ -79,7 +82,7 @@ fn count_words(ch: &Chapter) -> usize {
``` ```
## Enabling the backend ## Enabling the Backend
Now we've got the basics running, we want to actually use it. First, install the Now we've got the basics running, we want to actually use it. First, install the
program. program.
@ -180,7 +183,9 @@ $ cargo add serde serde_derive
And then you can create the config struct, And then you can create the config struct,
```rust ```rust
use serde_derive::{Serialize, Deserialize}; extern crate serde;
#[macro_use]
extern crate serde_derive;
... ...
@ -216,7 +221,7 @@ and then add a check to make sure we skip ignored chapters.
``` ```
## Output and signalling failure ## Output and Signalling Failure
While it's nice to print word counts to the terminal when a book is built, it While it's nice to print word counts to the terminal when a book is built, it
might also be a good idea to output them to a file somewhere. `mdbook` tells a might also be a good idea to output them to a file somewhere. `mdbook` tells a
@ -317,9 +322,9 @@ the "rule of silence" and only generate output when necessary (e.g. an error in
generation or a warning). generation or a warning).
All environment variables are passed through to the backend, allowing you to use All environment variables are passed through to the backend, allowing you to use
the usual `MDBOOK_LOG` to control logging verbosity. the usual `RUST_LOG` to control logging verbosity.
## Wrapping up ## Wrapping Up
Although contrived, hopefully this example was enough to show how you'd create Although contrived, hopefully this example was enough to show how you'd create
an alternative backend for `mdbook`. If you feel it's missing something, don't an alternative backend for `mdbook`. If you feel it's missing something, don't
@ -332,10 +337,10 @@ the source code or ask questions.
[Third Party Plugins]: https://github.com/rust-lang/mdBook/wiki/Third-party-plugins [Third Party Plugins]: https://github.com/rust-lang/mdBook/wiki/Third-party-plugins
[`RenderContext`]: https://docs.rs/mdbook-renderer/latest/mdbook_renderer/struct.RenderContext.html [`RenderContext`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
[`RenderContext::from_json()`]: https://docs.rs/mdbook-renderer/latest/mdbook_renderer/struct.RenderContext.html#method.from_json [`RenderContext::from_json()`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json
[`semver`]: https://crates.io/crates/semver [`semver`]: https://crates.io/crates/semver
[`Book`]: https://docs.rs/mdbook-renderer/latest/mdbook_renderer/book/struct.Book.html [`Book`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html
[`Book::iter()`]: https://docs.rs/mdbook-renderer/latest/mdbook_renderer/book/struct.Book.html#method.iter [`Book::iter()`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html#method.iter
[`Config`]: https://docs.rs/mdbook-renderer/latest/mdbook_renderer/config/struct.Config.html [`Config`]: https://docs.rs/mdbook/*/mdbook/config/struct.Config.html
[issue tracker]: https://github.com/rust-lang/mdBook/issues [issue tracker]: https://github.com/rust-lang/mdBook/issues

Some files were not shown because too many files have changed in this diff Show more