mirror of
https://github.com/tommilligan/mdbook-admonish.git
synced 2025-12-27 15:51:33 -05:00
Compare commits
56 Commits
better-err
...
bug-123
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02640dab1f | ||
|
|
771e9c9fd8 | ||
|
|
cce9343c47 | ||
|
|
20b158966b | ||
|
|
491f9cf341 | ||
|
|
6deaf1ea2b | ||
|
|
041e5a566f | ||
|
|
99b5a235cf | ||
|
|
39edc4d92a | ||
|
|
7773213093 | ||
|
|
e888fcd021 | ||
|
|
95dc7582ad | ||
|
|
b658eb6049 | ||
|
|
623291625a | ||
|
|
4dad5a86c8 | ||
|
|
7e774f4655 | ||
|
|
823cefbcbc | ||
|
|
a6a2941821 | ||
|
|
faf99a1b76 | ||
|
|
afdc2b03d0 | ||
|
|
e55df3e60b | ||
|
|
f3d49b93de | ||
|
|
92caf95b34 | ||
|
|
0742c6c1e8 | ||
|
|
60706be3e0 | ||
|
|
f5a6b9ef0f | ||
|
|
9361b7e7fa | ||
|
|
24bef47b15 | ||
|
|
0eb5fd35c3 | ||
|
|
de539cd0fd | ||
|
|
4842daea1c | ||
|
|
76212fccfb | ||
|
|
681c991a9a | ||
|
|
1e0a3992d5 | ||
|
|
d97747d195 | ||
|
|
2e2cebfc83 | ||
|
|
ab595f18f7 | ||
|
|
196585f4f2 | ||
|
|
7ad4d3f18c | ||
|
|
0324c93efa | ||
|
|
62dd36624d | ||
|
|
b3e82df34e | ||
|
|
e8813eb104 | ||
|
|
f606ad8758 | ||
|
|
d269838765 | ||
|
|
082359e562 | ||
|
|
84d163c32f | ||
|
|
90484a44ea | ||
|
|
5e7674d1f9 | ||
|
|
55a654dfca | ||
|
|
a2f664cac3 | ||
|
|
dfb70b0415 | ||
|
|
fb1b789386 | ||
|
|
97bcd97c64 | ||
|
|
a2a7316b26 | ||
|
|
f9eb198cd0 |
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- 1.60.0
|
||||
- 1.66.0
|
||||
experimental:
|
||||
- false
|
||||
# Run a canary test on nightly that's allowed to fail
|
||||
|
||||
64
.github/workflows/deploy.yml
vendored
64
.github/workflows/deploy.yml
vendored
@@ -16,14 +16,16 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- x86_64-unknown-linux-gnu
|
||||
- x86_64-unknown-linux-musl
|
||||
- x86_64-apple-darwin
|
||||
- x86_64-pc-windows-msvc
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
name: aarch64-unknown-linux-musl.tar.gz
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
# Deliberately pinned to the same version `mdbook` uses to build
|
||||
# binaries, so we use the same glibc version
|
||||
#
|
||||
# ref: https://github.com/rust-lang/mdBook/pull/1955
|
||||
os: ubuntu-20.04
|
||||
name: x86_64-unknown-linux-gnu.tar.gz
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
@@ -56,17 +58,19 @@ jobs:
|
||||
profile: minimal
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Setup | musl tools
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
run: sudo apt install -y musl-tools
|
||||
- name: Setup | cross
|
||||
if: endsWith(matrix.target, '-unknown-linux-musl')
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cross
|
||||
|
||||
- name: Build | Build
|
||||
if: matrix.target != 'x86_64-unknown-linux-musl'
|
||||
if: ${{ !endsWith(matrix.target, '-unknown-linux-musl') }}
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Build | Build (musl)
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
if: endsWith(matrix.target, '-unknown-linux-musl')
|
||||
run: cross build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Post Setup | Extract tag name
|
||||
shell: bash
|
||||
@@ -78,7 +82,6 @@ jobs:
|
||||
run: |
|
||||
mkdir target/stage
|
||||
cd target/${{ matrix.target }}/release
|
||||
strip ${{ env.CRATE_NAME }}.exe
|
||||
7z a ../../stage/${{ env.CRATE_NAME }}-${{ steps.extract_tag.outputs.tag }}-${{ matrix.name }} ${{ env.CRATE_NAME }}.exe
|
||||
cd -
|
||||
- name: Post Setup | Prepare artifacts [-nix]
|
||||
@@ -86,7 +89,6 @@ jobs:
|
||||
run: |
|
||||
mkdir target/stage
|
||||
cd target/${{ matrix.target }}/release
|
||||
strip ${{ env.CRATE_NAME }}
|
||||
tar czvf ../../stage/${{ env.CRATE_NAME }}-${{ steps.extract_tag.outputs.tag }}-${{ matrix.name }} ${{ env.CRATE_NAME }}
|
||||
cd -
|
||||
- name: Post Setup | Upload artifacts
|
||||
@@ -109,9 +111,14 @@ jobs:
|
||||
- name: Setup | Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Setup | Extract version
|
||||
shell: bash
|
||||
run: echo "##[set-output name=version;]$(echo ${GITHUB_REF#refs/tags/v})"
|
||||
id: extract_version
|
||||
|
||||
- name: Setup | Release notes
|
||||
run: |
|
||||
git log -1 --pretty='%s' > RELEASE.md
|
||||
cat CHANGELOG.md | sed -n '/^## ${{ steps.extract_version.outputs.version }}$/,/^## /p' | sed '$d' > RELEASE.md
|
||||
- name: Build | Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
@@ -119,3 +126,30 @@ jobs:
|
||||
body_path: RELEASE.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Upload to crates.io
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
needs: github_release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
~/.cargo/bin
|
||||
cargo_target
|
||||
# We reuse the cache from our detailed test environment, if available
|
||||
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Publish crate
|
||||
env:
|
||||
CARGO_LOGIN_TOKEN: ${{ secrets.CARGO_LOGIN_TOKEN }}
|
||||
run: ./scripts/publish
|
||||
|
||||
7
.github/workflows/docs.yml
vendored
7
.github/workflows/docs.yml
vendored
@@ -1,8 +1,9 @@
|
||||
name: docs
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -30,6 +31,8 @@ jobs:
|
||||
override: true
|
||||
- name: Install mdbook
|
||||
run: ./scripts/install-mdbook
|
||||
- name: Install mdbook extras
|
||||
run: ./book/scripts/install-mdbook-extras
|
||||
- name: Build book
|
||||
run: ./scripts/build-book
|
||||
- name: Push docs
|
||||
|
||||
31
.github/workflows/publish.yml
vendored
31
.github/workflows/publish.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
~/.cargo/bin
|
||||
cargo_target
|
||||
# We reuse the cache from our detailed test environment, if available
|
||||
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Publish crate
|
||||
env:
|
||||
CARGO_LOGIN_TOKEN: ${{ secrets.CARGO_LOGIN_TOKEN }}
|
||||
run: ./scripts/publish
|
||||
76
CHANGELOG.md
76
CHANGELOG.md
@@ -1,7 +1,73 @@
|
||||
## Changelog
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.11.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reverted internal dependency upgrades that unintentionally increased MSRV from 1.66.0 in 1.11.0
|
||||
|
||||
## 1.11.0 (yanked)
|
||||
|
||||
**Note:** This release has been yanked.
|
||||
|
||||
It unintentionally increased the MSRV from 1.66.0
|
||||
|
||||
### Changed
|
||||
|
||||
- `gnu` prebuilt binaries are now built on `ubuntu-20.04` to match `mdbook` binaries. Thanks to [@eitsupi](https://github.com/eitsupi) for the fix! ([#118](https://github.com/tommilligan/mdbook-admonish/pull/118))
|
||||
|
||||
### Added
|
||||
|
||||
- `aarch64-unknown-linux-musl` prebuilt binary now available ([#119](https://github.com/tommilligan/mdbook-admonish/pull/119))
|
||||
|
||||
## 1.10.2
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `cargo install mdbook-admonish` failing due to an internal dependency mismatch with `mdbook` ([#115](https://github.com/tommilligan/mdbook-admonish/pull/115))
|
||||
|
||||
## 1.10.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Only restyle `summary` elements generated by `mdbook-admonish`. Thanks to [@ImUrX](https://github.com/ImUrX) for the report and fix! ([#112](https://github.com/tommilligan/mdbook-admonish/pull/112))
|
||||
|
||||
## 1.10.0
|
||||
|
||||
### Changed
|
||||
|
||||
- MSRV (minimum supported rust version) is now 1.66.0 for mdbook v0.4.32 ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
|
||||
|
||||
### Added
|
||||
|
||||
- Support `mdbook test` running doctests inside admonish blocks. Opt-in to this by setting `renderer.test.action_mode = "strip"` ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
|
||||
- Log a warning when an invalid admonish block is encountered ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Document all `book.toml` configuration options [in the reference](https://tommilligan.github.io/mdbook-admonish/reference.html), some of which were previously undocumened ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
|
||||
|
||||
## 1.9.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Styles updated to `^2.0.1`. Run `mdbook-admonish install` to update.
|
||||
- MSRV (minimum supported rust version) is now 1.64.0 for clap v4 ([#79](https://github.com/tommilligan/mdbook-admonish/pull/79))
|
||||
- More verbose error messages for invalid TOML configurations ([#79](https://github.com/tommilligan/mdbook-admonish/pull/79))
|
||||
|
||||
### Added
|
||||
|
||||
- User can set book-wide default for title and collapsible properties ([#84](https://github.com/tommilligan/mdbook-admonish/pull/84)), thanks to [@ShaunSHamilton](https://github.com/ShaunSHamilton)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Custom installation and CSS directories are now normalized ([#49](https://github.com/tommilligan/mdbook-admonish/pull/49))
|
||||
- Fix title bars with no text rendering badly ([#83](https://github.com/tommilligan/mdbook-admonish/pull/83)), thanks to [@ShaunSHamilton](https://github.com/ShaunSHamilton)
|
||||
- Better error message display on crash ([#48](https://github.com/tommilligan/mdbook-admonish/pull/48))
|
||||
- Better support for commonmark code fence syntax ([#88](https://github.com/tommilligan/mdbook-admonish/pull/88), [#89](https://github.com/tommilligan/mdbook-admonish/pull/89))
|
||||
|
||||
## 1.8.0
|
||||
|
||||
### Changed
|
||||
@@ -17,7 +83,7 @@
|
||||
### Added
|
||||
|
||||
- Support key/value configuration ([#24](https://github.com/tommilligan/mdbook-admonish/pull/24), thanks [@gggto](https://github.com/gggto) and [@schungx](https://github.com/schungx) for design input)
|
||||
- Support collapsiable admonition bodies ([#26](https://github.com/tommilligan/mdbook-admonish/pull/26), thanks [@gggto](https://github.com/gggto) for the suggestion and implementation!)
|
||||
- Support collapsible admonition bodies ([#26](https://github.com/tommilligan/mdbook-admonish/pull/26), thanks [@gggto](https://github.com/gggto) for the suggestion and implementation!)
|
||||
- Make anchor links hoverable ([#27](https://github.com/tommilligan/mdbook-admonish/pull/27))
|
||||
- Better handling for misconfigured admonitions ([#25](https://github.com/tommilligan/mdbook-admonish/pull/25))
|
||||
- Nicer in-book error messages
|
||||
@@ -90,7 +156,11 @@ This behaviour is [documented in the readme here](https://github.com/tommilligan
|
||||
- Flattened indentation of generated HTML, otherwise it's styled as a markdown code block
|
||||
- Fixed edge cases where the info string changes length when parsed, causing title/body to be incorrectly split
|
||||
|
||||
## 1.3.0
|
||||
## 1.3.0 (yanked)
|
||||
|
||||
**Note:** This release has been yanked.
|
||||
|
||||
It unintentionally introduced a serious parsing bug.
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
1420
Cargo.lock
generated
1420
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
36
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook-admonish"
|
||||
version = "1.8.0"
|
||||
version = "1.11.1"
|
||||
edition = "2021"
|
||||
|
||||
authors = ["Tom Milligan <code@tommilligan.net>"]
|
||||
@@ -17,32 +17,38 @@ name = "mdbook-admonish"
|
||||
path = "src/bin/mdbook-admonish.rs"
|
||||
required-features = ["cli"]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
[lib]
|
||||
name = "mdbook_admonish"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.65"
|
||||
anyhow = "1.0.72"
|
||||
clap = { version = "4", default_features = false, features = ["std", "derive"], optional = true }
|
||||
env_logger = { version = "0.9.1", default_features = false, optional = true }
|
||||
log = { version = "0.4.17", optional = true }
|
||||
mdbook = "0.4.21"
|
||||
once_cell = "1.15.0"
|
||||
pulldown-cmark = "0.9.2"
|
||||
regex = "1.6.0"
|
||||
semver = "1.0.14"
|
||||
serde = { version = "1.0.145", features = ["derive"] }
|
||||
serde_json = "1.0.85"
|
||||
toml = "0.5.9"
|
||||
toml_edit = { version = "0.15.0", optional = true }
|
||||
env_logger = { version = "0.10", default_features = false, optional = true }
|
||||
log = "0.4.19"
|
||||
mdbook = "0.4.34"
|
||||
once_cell = "1.18.0"
|
||||
pulldown-cmark = "0.9.3"
|
||||
regex = "1.9.3"
|
||||
semver = "1.0.18"
|
||||
serde = { version = "1.0.183", features = ["derive"] }
|
||||
serde_json = "1.0.104"
|
||||
# The version of toml that mdbook uses internally (and uses in it's public api)
|
||||
# Only used for compatilibilty with the mdbook public api
|
||||
toml_mdbook = { package = "toml", version = "0.5.11" }
|
||||
toml = "0.7.6"
|
||||
toml_edit = { version = "0.19.14", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.3.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
|
||||
[features]
|
||||
default = ["cli", "cli-install"]
|
||||
|
||||
# Enable the command line binary
|
||||
cli = ["clap", "env_logger", "log"]
|
||||
cli = ["clap", "env_logger"]
|
||||
# Enable installation of files and configuration
|
||||
cli-install = ["toml_edit"]
|
||||
|
||||
12
README.md
12
README.md
@@ -123,18 +123,16 @@ Alternatively, pin to a specific version for a reproducible installation:
|
||||
cargo install mdbook-admonish --vers "1.5.0" --locked
|
||||
```
|
||||
|
||||
### Bail on error
|
||||
### Process included files
|
||||
|
||||
By default, if an adomnition is incorrectly configured, an error will be shown in the book.
|
||||
|
||||
You can force it to break the build instead, with the following configuration:
|
||||
You can ensure that content inlined with `{{#include}}` is also processed by [setting the `after` option](https://rust-lang.github.io/mdBook/format/configuration/preprocessors.html#require-a-certain-order):
|
||||
|
||||
```toml
|
||||
[preprocessor.admonish]
|
||||
on_failure = "bail"
|
||||
after = ["links"]
|
||||
```
|
||||
|
||||
This may be useful for non-interative workflows.
|
||||
This will expand `include` directives, before expanding `admonish` blocks.
|
||||
|
||||
### Semantic Versioning
|
||||
|
||||
@@ -176,8 +174,6 @@ You must make the next `mdbook-admonish` crate version at least a **minor** vers
|
||||
|
||||
Github workflows are setup such that pushing a `vX.Y.Z` tag will trigger a release to be cut.
|
||||
|
||||
Once the release is created, copy and paste the relevant section of `CHANGELOG.md` manually to update the description.
|
||||
|
||||
## Thanks
|
||||
|
||||
This utility is heavily drawn from and inspired by other projects, namely:
|
||||
|
||||
@@ -9,9 +9,13 @@ title = "The mdbook-admonish book"
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
|
||||
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`
|
||||
|
||||
[preprocessor.toc]
|
||||
command = "mdbook-toc"
|
||||
renderer = ["html"]
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["././mdbook-admonish.css"]
|
||||
additional-css = ["./mdbook-admonish.css"]
|
||||
|
||||
9
book/scripts/install-mdbook-extras
Executable file
9
book/scripts/install-mdbook-extras
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -exuo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/../..
|
||||
|
||||
if ! mdbook-toc --version; then
|
||||
cargo install mdbook-toc --version 0.14.1 --force
|
||||
fi
|
||||
@@ -1,9 +1,13 @@
|
||||
# mdbook-admonish
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Intoduction
|
||||
|
||||
[](https://crates.io/crates/mdbook-admonish)
|
||||
[](https://docs.rs/mdbook-admonish)
|
||||
|
||||
A preprocessor for [mdbook](https://github.com/rust-lang-nursery/mdBook) to add [Material Design](https://material.io/design) admonishments, based on the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) implementation.
|
||||
A preprocessor for [mdbook](https://github.com/rust-lang/mdBook) to add [Material Design](https://material.io/design) admonishments, based on the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) implementation.
|
||||
|
||||
It turns this:
|
||||
|
||||
@@ -21,6 +25,8 @@ A beautifully styled message.
|
||||
|
||||
## Usage
|
||||
|
||||
### A basic `admonish` block
|
||||
|
||||
Use any [fenced code-block](https://spec.commonmark.org/0.30/#fenced-code-blocks) as you normally would, but annotate it with `admonish <admonition type>`:
|
||||
|
||||
````
|
||||
@@ -33,7 +39,7 @@ My example is the best!
|
||||
My example is the best!
|
||||
```
|
||||
|
||||
See the [mkdocs-material docs](https://squidfunk.github.io/mkdocs-material/reference/admonitions/#supported-types) for a list of supported admonitions. You'll find:
|
||||
See the [list of directives](./reference.md#directives) for a full list of supported admonitions. You'll find:
|
||||
|
||||
- `info`
|
||||
- `warning`
|
||||
@@ -54,8 +60,28 @@ A plain note.
|
||||
A plain note.
|
||||
```
|
||||
|
||||
### Invalid blocks
|
||||
|
||||
By default, if an `admonish` block cannot be parsed, an error will be rendered in the output:
|
||||
|
||||
````
|
||||
```admonish title="\j"
|
||||
This block will error
|
||||
```
|
||||
````
|
||||
|
||||
```admonish title="\j"
|
||||
This block will error
|
||||
```
|
||||
|
||||
You can also configure the build to fail loudly, by setting `on_failure = "bail"` in `book.toml`. See the [configuration reference](./reference.md#booktoml-configuration) for more details.
|
||||
|
||||
### Additional Options
|
||||
|
||||
You can pass additional options to each block. The options are structured as TOML key-value pairs.
|
||||
|
||||
Note that some options can be passed globally, through the `default` section in `book.toml`. See the [configuration reference](./reference.md#booktoml-configuration) for more details.
|
||||
|
||||
#### Custom title
|
||||
|
||||
A custom title can be provided, contained in a double quoted TOML string.
|
||||
@@ -150,17 +176,3 @@ Will yield something like the following HTML, which you can then apply styles to
|
||||
```admonish collapsible=true
|
||||
Content will be hidden initially.
|
||||
```
|
||||
|
||||
#### Invalid blocks
|
||||
|
||||
If a rendering error occurs, an error will be rendered in the output:
|
||||
|
||||
````
|
||||
```admonish title="\j"
|
||||
This block will error
|
||||
```
|
||||
````
|
||||
|
||||
```admonish title="\j"
|
||||
This block will error
|
||||
```
|
||||
|
||||
@@ -1,5 +1,93 @@
|
||||
# Reference
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## `book.toml` configuration
|
||||
|
||||
See below for all configuration options available to add in `book.toml`.
|
||||
|
||||
The options should all be nested under `preprocessor.admonish`; for example:
|
||||
|
||||
```toml
|
||||
[preprocessor.admonish]
|
||||
on_failure = "bail"
|
||||
|
||||
[preprocessor.admonish.default]
|
||||
collapsible = true
|
||||
|
||||
[preprocessor.admonish.renderer.test]
|
||||
render_mode = "strip"
|
||||
```
|
||||
|
||||
### `on_failure`
|
||||
|
||||
Optional. Default value: `continue`.
|
||||
|
||||
The action to take when an invalid `admonish` block is encountered:
|
||||
|
||||
- `continue` (default): Continue processing future blocks, do not fail the build. If rendering to HTML, an error message will be displayed in the book output.
|
||||
- `bail`: Abort the build.
|
||||
|
||||
### `default`
|
||||
|
||||
Optional.
|
||||
|
||||
Default values to use, when not provided in an `admonish` block explicitly.
|
||||
|
||||
Subfields:
|
||||
|
||||
- `default.title` (optional): Title to use for blocks. Defaults to the directive used in titlecase.
|
||||
- `default.collapsible` (optional, default: `false`): Make blocks collapsible by default when set to `true`.
|
||||
|
||||
### `renderer`
|
||||
|
||||
````admonish tip
|
||||
It is recommended that you set:
|
||||
|
||||
```toml
|
||||
[preprocessor.admonish.renderer.test]
|
||||
render_mode = "strip"
|
||||
```
|
||||
|
||||
This allows `mdbook test` to find and test rust examples within `admonish` blocks.
|
||||
|
||||
This will be the default behaviour in the next `mdbook-admonish` major version.
|
||||
````
|
||||
|
||||
Optional.
|
||||
|
||||
Additional settings to apply, depending on the renderer that is running.
|
||||
|
||||
The most common renderers used are:
|
||||
|
||||
- `html`: Used by `mdbook build` to build the final book output.
|
||||
- `test`: Used by `mdbook test` to find and run doctests.
|
||||
|
||||
Subfields:
|
||||
|
||||
- `renderer.<renderer_name>.render_mode` (optional): The action `mdbook-admonish` should take when running with this renderer.
|
||||
- Valid values:
|
||||
- `html`: Convert `admonish` blocks into HTML output.
|
||||
- `preserve`: Do nothing. Leave the book untouched.
|
||||
- `strip`: Strip `admonish`-specific syntax, leaving the inner content untouched.
|
||||
- Default values:
|
||||
- For the `html` renderer, the default value is `html`.
|
||||
- For all other renderers, the default value is `preserve`.
|
||||
|
||||
### `command`
|
||||
|
||||
Required.
|
||||
|
||||
Used by `mdbook` to know how to call the `mdbook-admonish` plugin.
|
||||
|
||||
Running this command with the `--version` flag from your shell should work, for the plugin to function.
|
||||
|
||||
### `assets_version`
|
||||
|
||||
Optional.
|
||||
|
||||
This is automatically updated by `mdbook-admonish install` and should not be edited.
|
||||
|
||||
## Directives
|
||||
|
||||
All supported directives are listed below.
|
||||
|
||||
6
book/v2.md
Normal file
6
book/v2.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Notes for a v2 release
|
||||
|
||||
## Default behaviour changes
|
||||
|
||||
- `on_failure` default changed from `continue` to `bail`
|
||||
- `preprocessor.admonish.renderer.test.render_mode` default changed from `preserve` to `strip`
|
||||
@@ -143,8 +143,9 @@ a.admonition-anchor-link {
|
||||
}
|
||||
|
||||
// Admonition title
|
||||
:is(.admonition-title, summary) {
|
||||
:is(.admonition-title, summary.admonition-title) {
|
||||
position: relative;
|
||||
min-height: 4rem;
|
||||
margin-block: 0;
|
||||
margin-inline: -1.6rem -1.2rem;
|
||||
padding-block: 0.8rem;
|
||||
@@ -232,7 +233,7 @@ summary.admonition-title {
|
||||
}
|
||||
|
||||
// Admonition flavour title
|
||||
:is(#{$flavours}) > :is(.admonition-title, summary) {
|
||||
:is(#{$flavours}) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: color.adjust($tint, $alpha: -0.9);
|
||||
|
||||
// Admonition icon
|
||||
|
||||
@@ -9,9 +9,13 @@ title = "mdbook-admonish-integration"
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
|
||||
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`
|
||||
after = ["links"]
|
||||
|
||||
[preprocessor.admonish.renderer.test]
|
||||
render_mode = "strip"
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["././mdbook-admonish.css"]
|
||||
additional-css = ["./mdbook-admonish.css"]
|
||||
|
||||
@@ -9,9 +9,13 @@ title = "mdbook-admonish-integration"
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
|
||||
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`
|
||||
after = ["links"]
|
||||
|
||||
[preprocessor.admonish.renderer.test]
|
||||
render_mode = "strip"
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["././mdbook-admonish.css"]
|
||||
additional-css = ["./mdbook-admonish.css"]
|
||||
|
||||
@@ -29,9 +29,16 @@
|
||||
<p><a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Failed with: TOML parsing error: unterminated string at line 1 column 7</p>
|
||||
<p>Failed with:</p>
|
||||
<pre><code class="language-log">TOML parsing error: TOML parse error at line 1, column 8
|
||||
|
|
||||
1 | title="
|
||||
| ^
|
||||
invalid basic string
|
||||
|
||||
</code></pre>
|
||||
<p>Original markdown input:</p>
|
||||
<pre><code>```admonish title="
|
||||
<pre><code class="language-markdown">```admonish title="
|
||||
No title, only body
|
||||
```
|
||||
</code></pre>
|
||||
@@ -46,4 +53,66 @@ No title, only body
|
||||
<p>Hidden on load</p>
|
||||
</div>
|
||||
</details>
|
||||
<div id="admonition-warning" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
<p>Warning</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-warning"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<p>This is a commonly shared warning!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-note-2" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<p>Note</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note-2"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<pre><code class="language-bash">Nested code block
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-note-3" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<p>Note</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note-3"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
|
||||
</span><span class="boring">fn main() {
|
||||
</span>let x = 10;
|
||||
x = 20;
|
||||
<span class="boring">}</span></code></pre></pre>
|
||||
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
|
||||
</span><span class="boring">fn main() {
|
||||
</span>let x = 10;
|
||||
let x = 20;
|
||||
<span class="boring">}</span></code></pre></pre>
|
||||
</div>
|
||||
</div>
|
||||
<p>In a list:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Thing one</p>
|
||||
<pre><code class="language-sh">Thing one
|
||||
</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Thing two</p>
|
||||
<div id="admonition-note-4" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<p>Note</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note-4"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Thing two</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<p>Thing three</p>
|
||||
<pre><code class="language-sh">Thing three
|
||||
</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
|
||||
@@ -51,7 +51,21 @@ if [ "$DIFF_RESULT" != 0 ]; then
|
||||
eprintln "error: generated html was different than expected"
|
||||
eprintln ""
|
||||
eprintln "error: If you expected the output to change, run:"
|
||||
eprintln "./integration/update-snapshot"
|
||||
eprintln "./integration/scripts/update-snapshot"
|
||||
eprintln "and commit the result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eprintln "Verifying mdbook test runs doctests"
|
||||
set +e
|
||||
TEST_RESULT="$(mdbook test 2>&1 | grep "1 passed; 1 failed")"
|
||||
set -e
|
||||
|
||||
if [[ "$TEST_RESULT" != "test result: FAILED. 1 passed; 1 failed;"* ]]; then
|
||||
eprintln ""
|
||||
eprintln "error: mdbook test did not complete as expected"
|
||||
eprintln ""
|
||||
eprintln "Full output:"
|
||||
mdbook test
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -21,3 +21,43 @@ No title, only body
|
||||
```admonish collapsible=true
|
||||
Hidden on load
|
||||
```
|
||||
|
||||
{{#include common_warning.md}}
|
||||
|
||||
````admonish
|
||||
```bash
|
||||
Nested code block
|
||||
```
|
||||
````
|
||||
|
||||
````admonish
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
|
||||
```rust
|
||||
let x = 10;
|
||||
let x = 20;
|
||||
```
|
||||
````
|
||||
|
||||
In a list:
|
||||
|
||||
1. Thing one
|
||||
|
||||
```sh
|
||||
Thing one
|
||||
```
|
||||
|
||||
1. Thing two
|
||||
|
||||
```admonish
|
||||
Thing two
|
||||
```
|
||||
|
||||
1. Thing three
|
||||
|
||||
```sh
|
||||
Thing three
|
||||
```
|
||||
|
||||
3
integration/src/common_warning.md
Normal file
3
integration/src/common_warning.md
Normal file
@@ -0,0 +1,3 @@
|
||||
```admonish warning
|
||||
This is a commonly shared warning!
|
||||
```
|
||||
14
qvet.yml
Normal file
14
qvet.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
action:
|
||||
ready:
|
||||
type: link
|
||||
name: "Push New Version Tag"
|
||||
url: "https://github.com/tommilligan/mdbook-admonish"
|
||||
|
||||
commit:
|
||||
ignore:
|
||||
merges: true
|
||||
|
||||
release:
|
||||
identifiers:
|
||||
- type: tag
|
||||
pattern: "^v[0-9]"
|
||||
@@ -11,16 +11,6 @@ function eprintln() {
|
||||
eprintln "Formatting sources"
|
||||
cargo fmt -- --check
|
||||
|
||||
# Known issues:
|
||||
# - RUSTSEC-2020-0071 known unlikely segfault in `time`
|
||||
# - RUSTSEC-2020-0016 `net2` is unmaintained
|
||||
# - RUSTSEC-2020-0159 known unlikely segfault in `chrono`
|
||||
eprintln "Auditing dependencies"
|
||||
cargo audit --deny warnings \
|
||||
--ignore RUSTSEC-2020-0071 \
|
||||
--ignore RUSTSEC-2020-0016 \
|
||||
--ignore RUSTSEC-2020-0159
|
||||
|
||||
eprintln "Linting sources"
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
|
||||
|
||||
@@ -10,8 +10,4 @@ cd "$(dirname "$0")"/..
|
||||
|
||||
rustup component add rustfmt clippy
|
||||
|
||||
if ! cargo audit --version; then
|
||||
cargo install cargo-audit --force
|
||||
fi
|
||||
|
||||
./scripts/install-mdbook
|
||||
|
||||
@@ -5,5 +5,5 @@ set -exuo pipefail
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
if ! mdbook --version; then
|
||||
cargo install mdbook --force
|
||||
cargo install mdbook --version 0.4.32 --force
|
||||
fi
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.0.0
|
||||
2.0.2
|
||||
|
||||
@@ -75,8 +75,9 @@ a.admonition-anchor-link::before {
|
||||
content: "§";
|
||||
}
|
||||
|
||||
:is(.admonition-title, summary) {
|
||||
:is(.admonition-title, summary.admonition-title) {
|
||||
position: relative;
|
||||
min-height: 4rem;
|
||||
margin-block: 0;
|
||||
margin-inline: -1.6rem -1.2rem;
|
||||
padding-block: 0.8rem;
|
||||
@@ -85,13 +86,13 @@ a.admonition-anchor-link::before {
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
display: flex;
|
||||
}
|
||||
:is(.admonition-title, summary) p {
|
||||
:is(.admonition-title, summary.admonition-title) p {
|
||||
margin: 0;
|
||||
}
|
||||
html :is(.admonition-title, summary):last-child {
|
||||
html :is(.admonition-title, summary.admonition-title):last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:is(.admonition-title, summary)::before {
|
||||
:is(.admonition-title, summary.admonition-title)::before {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-start: 1.6rem;
|
||||
@@ -106,7 +107,7 @@ html :is(.admonition-title, summary):last-child {
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
}
|
||||
:is(.admonition-title, summary):hover a.admonition-anchor-link {
|
||||
:is(.admonition-title, summary.admonition-title):hover a.admonition-anchor-link {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
@@ -135,10 +136,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #448aff;
|
||||
}
|
||||
|
||||
:is(.note) > :is(.admonition-title, summary) {
|
||||
:is(.note) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
}
|
||||
:is(.note) > :is(.admonition-title, summary)::before {
|
||||
:is(.note) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #448aff;
|
||||
mask-image: var(--md-admonition-icon--note);
|
||||
-webkit-mask-image: var(--md-admonition-icon--note);
|
||||
@@ -152,10 +153,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #00b0ff;
|
||||
}
|
||||
|
||||
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary) {
|
||||
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 176, 255, 0.1);
|
||||
}
|
||||
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary)::before {
|
||||
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00b0ff;
|
||||
mask-image: var(--md-admonition-icon--abstract);
|
||||
-webkit-mask-image: var(--md-admonition-icon--abstract);
|
||||
@@ -169,10 +170,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #00b8d4;
|
||||
}
|
||||
|
||||
:is(.info, .todo) > :is(.admonition-title, summary) {
|
||||
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 184, 212, 0.1);
|
||||
}
|
||||
:is(.info, .todo) > :is(.admonition-title, summary)::before {
|
||||
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00b8d4;
|
||||
mask-image: var(--md-admonition-icon--info);
|
||||
-webkit-mask-image: var(--md-admonition-icon--info);
|
||||
@@ -186,10 +187,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #00bfa5;
|
||||
}
|
||||
|
||||
:is(.tip, .hint, .important) > :is(.admonition-title, summary) {
|
||||
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 191, 165, 0.1);
|
||||
}
|
||||
:is(.tip, .hint, .important) > :is(.admonition-title, summary)::before {
|
||||
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00bfa5;
|
||||
mask-image: var(--md-admonition-icon--tip);
|
||||
-webkit-mask-image: var(--md-admonition-icon--tip);
|
||||
@@ -203,10 +204,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #00c853;
|
||||
}
|
||||
|
||||
:is(.success, .check, .done) > :is(.admonition-title, summary) {
|
||||
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 200, 83, 0.1);
|
||||
}
|
||||
:is(.success, .check, .done) > :is(.admonition-title, summary)::before {
|
||||
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00c853;
|
||||
mask-image: var(--md-admonition-icon--success);
|
||||
-webkit-mask-image: var(--md-admonition-icon--success);
|
||||
@@ -220,10 +221,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #64dd17;
|
||||
}
|
||||
|
||||
:is(.question, .help, .faq) > :is(.admonition-title, summary) {
|
||||
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(100, 221, 23, 0.1);
|
||||
}
|
||||
:is(.question, .help, .faq) > :is(.admonition-title, summary)::before {
|
||||
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #64dd17;
|
||||
mask-image: var(--md-admonition-icon--question);
|
||||
-webkit-mask-image: var(--md-admonition-icon--question);
|
||||
@@ -237,10 +238,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #ff9100;
|
||||
}
|
||||
|
||||
:is(.warning, .caution, .attention) > :is(.admonition-title, summary) {
|
||||
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 145, 0, 0.1);
|
||||
}
|
||||
:is(.warning, .caution, .attention) > :is(.admonition-title, summary)::before {
|
||||
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff9100;
|
||||
mask-image: var(--md-admonition-icon--warning);
|
||||
-webkit-mask-image: var(--md-admonition-icon--warning);
|
||||
@@ -254,10 +255,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #ff5252;
|
||||
}
|
||||
|
||||
:is(.failure, .fail, .missing) > :is(.admonition-title, summary) {
|
||||
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 82, 82, 0.1);
|
||||
}
|
||||
:is(.failure, .fail, .missing) > :is(.admonition-title, summary)::before {
|
||||
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff5252;
|
||||
mask-image: var(--md-admonition-icon--failure);
|
||||
-webkit-mask-image: var(--md-admonition-icon--failure);
|
||||
@@ -271,10 +272,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #ff1744;
|
||||
}
|
||||
|
||||
:is(.danger, .error) > :is(.admonition-title, summary) {
|
||||
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 23, 68, 0.1);
|
||||
}
|
||||
:is(.danger, .error) > :is(.admonition-title, summary)::before {
|
||||
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff1744;
|
||||
mask-image: var(--md-admonition-icon--danger);
|
||||
-webkit-mask-image: var(--md-admonition-icon--danger);
|
||||
@@ -288,10 +289,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #f50057;
|
||||
}
|
||||
|
||||
:is(.bug) > :is(.admonition-title, summary) {
|
||||
:is(.bug) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(245, 0, 87, 0.1);
|
||||
}
|
||||
:is(.bug) > :is(.admonition-title, summary)::before {
|
||||
:is(.bug) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #f50057;
|
||||
mask-image: var(--md-admonition-icon--bug);
|
||||
-webkit-mask-image: var(--md-admonition-icon--bug);
|
||||
@@ -305,10 +306,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #7c4dff;
|
||||
}
|
||||
|
||||
:is(.example) > :is(.admonition-title, summary) {
|
||||
:is(.example) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(124, 77, 255, 0.1);
|
||||
}
|
||||
:is(.example) > :is(.admonition-title, summary)::before {
|
||||
:is(.example) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #7c4dff;
|
||||
mask-image: var(--md-admonition-icon--example);
|
||||
-webkit-mask-image: var(--md-admonition-icon--example);
|
||||
@@ -322,10 +323,10 @@ details[open].admonition > summary.admonition-title::after {
|
||||
border-color: #9e9e9e;
|
||||
}
|
||||
|
||||
:is(.quote, .cite) > :is(.admonition-title, summary) {
|
||||
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(158, 158, 158, 0.1);
|
||||
}
|
||||
:is(.quote, .cite) > :is(.admonition-title, summary)::before {
|
||||
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #9e9e9e;
|
||||
mask-image: var(--md-admonition-icon--quote);
|
||||
-webkit-mask-image: var(--md-admonition-icon--quote);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use mdbook::{
|
||||
errors::Error,
|
||||
preprocess::{CmdPreprocessor, Preprocessor},
|
||||
@@ -7,8 +9,6 @@ use mdbook_admonish::Admonish;
|
||||
use std::path::PathBuf;
|
||||
use std::{io, process};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
/// mdbook preprocessor to add support for admonitions
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
@@ -26,9 +26,13 @@ enum Commands {
|
||||
/// Install the required assset files and include it in the config
|
||||
Install {
|
||||
/// Root directory for the book, should contain the configuration file (`book.toml`)
|
||||
///
|
||||
/// If not set, defaults to the current directory.
|
||||
dir: Option<PathBuf>,
|
||||
|
||||
/// Relative directory for the css assets, from the book directory root
|
||||
///
|
||||
/// If not set, defaults to the current directory.
|
||||
#[arg(long)]
|
||||
css_dir: Option<PathBuf>,
|
||||
},
|
||||
@@ -38,24 +42,26 @@ fn main() {
|
||||
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
None => {
|
||||
if let Err(e) = handle_preprocessing() {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
if let Err(error) = run(cli) {
|
||||
log::error!("Fatal error: {}", error);
|
||||
for error in error.chain() {
|
||||
log::error!(" - {}", error);
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(cli: Cli) -> Result<()> {
|
||||
match cli.command {
|
||||
None => handle_preprocessing(),
|
||||
Some(Commands::Supports { renderer }) => {
|
||||
handle_supports(renderer);
|
||||
}
|
||||
#[cfg(feature = "cli-install")]
|
||||
Some(Commands::Install { dir, css_dir }) => {
|
||||
install::handle_install(
|
||||
dir.unwrap_or_else(|| PathBuf::from(".")),
|
||||
css_dir.unwrap_or_else(|| PathBuf::from(".")),
|
||||
);
|
||||
}
|
||||
Some(Commands::Install { dir, css_dir }) => install::handle_install(
|
||||
dir.unwrap_or_else(|| PathBuf::from(".")),
|
||||
css_dir.unwrap_or_else(|| PathBuf::from(".")),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,11 +96,11 @@ fn handle_supports(renderer: String) -> ! {
|
||||
|
||||
#[cfg(feature = "cli-install")]
|
||||
mod install {
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
process,
|
||||
};
|
||||
use toml_edit::{self, Array, Document, Item, Table, Value};
|
||||
|
||||
@@ -116,19 +122,14 @@ mod install {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_install(proj_dir: PathBuf, css_dir: PathBuf) {
|
||||
pub fn handle_install(proj_dir: PathBuf, css_dir: PathBuf) -> Result<()> {
|
||||
let config = proj_dir.join("book.toml");
|
||||
|
||||
if !config.exists() {
|
||||
log::error!("Configuration file '{}' missing", config.display());
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
log::info!("Reading configuration file '{}'", config.display());
|
||||
let toml = fs::read_to_string(&config).expect("can't read configuration file");
|
||||
let toml = fs::read_to_string(&config)
|
||||
.with_context(|| format!("can't read configuration file '{}'", config.display()))?;
|
||||
let mut doc = toml
|
||||
.parse::<Document>()
|
||||
.expect("configuration is not valid TOML");
|
||||
.context("configuration is not valid TOML")?;
|
||||
|
||||
if let Ok(preprocessor) = preprocessor(&mut doc) {
|
||||
const ASSETS_VERSION: &str = std::include_str!("./assets/VERSION");
|
||||
@@ -143,8 +144,11 @@ mod install {
|
||||
|
||||
let mut additional_css = additional_css(&mut doc);
|
||||
for (name, content) in ADMONISH_CSS_FILES {
|
||||
let filepath = proj_dir.join(&css_dir).join(name);
|
||||
let filepath_str = filepath.to_str().expect("non-utf8 filepath");
|
||||
let filepath = proj_dir.join(css_dir.clone()).join(name);
|
||||
// Normalize path to remove no-op components
|
||||
// https://github.com/tommilligan/mdbook-admonish/issues/47
|
||||
let filepath: PathBuf = filepath.components().collect();
|
||||
let filepath_str = filepath.to_str().context("non-utf8 filepath")?;
|
||||
|
||||
if let Ok(ref mut additional_css) = additional_css {
|
||||
if !additional_css.contains_str(filepath_str) {
|
||||
@@ -159,18 +163,18 @@ mod install {
|
||||
"Copying '{name}' to '{filepath}'",
|
||||
filepath = filepath.display()
|
||||
);
|
||||
let mut file = File::create(filepath).expect("can't open file for writing");
|
||||
let mut file = File::create(&filepath).context("can't open file for writing")?;
|
||||
file.write_all(content)
|
||||
.expect("can't write content to file");
|
||||
.context("can't write content to file")?;
|
||||
}
|
||||
|
||||
let new_toml = doc.to_string();
|
||||
if new_toml != toml {
|
||||
log::info!("Saving changed configuration to '{}'", config.display());
|
||||
let mut file =
|
||||
File::create(config).expect("can't open configuration file for writing.");
|
||||
File::create(config).context("can't open configuration file for writing.")?;
|
||||
file.write_all(new_toml.as_bytes())
|
||||
.expect("can't write configuration");
|
||||
.context("can't write configuration")?;
|
||||
} else {
|
||||
log::info!("Configuration '{}' already up to date", config.display());
|
||||
}
|
||||
@@ -180,8 +184,7 @@ mod install {
|
||||
A beautifully styled message.
|
||||
```"#;
|
||||
log::info!("Add a code block like:\n{}", codeblock);
|
||||
|
||||
process::exit(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the `additional-css` field, initializing if required.
|
||||
|
||||
60
src/book_config.rs
Normal file
60
src/book_config.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook::preprocess::PreprocessorContext;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::types::AdmonitionDefaults;
|
||||
|
||||
/// Loads the plugin configuration from mdbook internals.
|
||||
///
|
||||
/// Roundtrips config to string, to avoid linking the plugin's internal version of toml
|
||||
/// to the one publically exposed by the mdbook library.
|
||||
pub(crate) fn admonish_config_from_context(ctx: &PreprocessorContext) -> Result<Config> {
|
||||
let table: String = toml_mdbook::to_string(
|
||||
ctx.config
|
||||
.get_preprocessor("admonish")
|
||||
.context("No configuration for mdbook-admonish in book.toml")?,
|
||||
)?;
|
||||
toml::from_str(&table).context("Invalid mdbook-admonish configuration in book.toml")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct Config {
|
||||
#[serde(default)]
|
||||
pub on_failure: OnFailure,
|
||||
|
||||
#[serde(default)]
|
||||
pub default: AdmonitionDefaults,
|
||||
|
||||
#[serde(default)]
|
||||
pub renderer: HashMap<String, RendererConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub assets_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct RendererConfig {
|
||||
pub render_mode: Option<RenderMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum RenderMode {
|
||||
Preserve,
|
||||
Strip,
|
||||
Html,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum OnFailure {
|
||||
Bail,
|
||||
Continue,
|
||||
}
|
||||
|
||||
impl Default for OnFailure {
|
||||
fn default() -> Self {
|
||||
Self::Continue
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
use crate::types::Directive;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod v1;
|
||||
mod v2;
|
||||
|
||||
/// Configuration as described by the instance of an admonition in markdown.
|
||||
///
|
||||
/// This structure represents the configuration the user must provide in each
|
||||
/// instance.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct AdmonitionInfoRaw {
|
||||
directive: String,
|
||||
title: Option<String>,
|
||||
additional_classnames: Vec<String>,
|
||||
collapsible: bool,
|
||||
pub(crate) struct InstanceConfig {
|
||||
pub(crate) directive: String,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) additional_classnames: Vec<String>,
|
||||
pub(crate) collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
/// Extract the remaining info string, if this is an admonition block.
|
||||
@@ -27,10 +28,10 @@ fn admonition_config_string(info_string: &str) -> Option<&str> {
|
||||
}
|
||||
}
|
||||
|
||||
impl AdmonitionInfoRaw {
|
||||
impl InstanceConfig {
|
||||
/// Returns:
|
||||
/// - `None` if this is not an `admonish` block.
|
||||
/// - `Some(AdmonitionInfoRaw)` if this is an `admonish` block
|
||||
/// - `Some(InstanceConfig)` if this is an `admonish` block
|
||||
pub fn from_info_string(info_string: &str) -> Option<Result<Self, String>> {
|
||||
let config_string = admonition_config_string(info_string)?;
|
||||
|
||||
@@ -40,65 +41,13 @@ impl AdmonitionInfoRaw {
|
||||
Err(config) => config,
|
||||
};
|
||||
|
||||
Some(
|
||||
if let Ok(info_raw) = v1::from_config_string(config_string) {
|
||||
// If we succeed at parsing v1, return that.
|
||||
Ok(info_raw)
|
||||
} else {
|
||||
// Otherwise return our v2 error.
|
||||
Err(config_v2_error)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct AdmonitionInfo {
|
||||
pub directive: Directive,
|
||||
pub title: Option<String>,
|
||||
pub additional_classnames: Vec<String>,
|
||||
pub collapsible: bool,
|
||||
}
|
||||
|
||||
impl AdmonitionInfo {
|
||||
pub fn from_info_string(info_string: &str) -> Option<Result<Self, String>> {
|
||||
AdmonitionInfoRaw::from_info_string(info_string).map(|result| result.map(Into::into))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdmonitionInfoRaw> for AdmonitionInfo {
|
||||
fn from(other: AdmonitionInfoRaw) -> Self {
|
||||
let AdmonitionInfoRaw {
|
||||
directive: raw_directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
} = other;
|
||||
let (directive, title) = match (Directive::from_str(&raw_directive), title) {
|
||||
(Ok(directive), None) => (directive, ucfirst(&raw_directive)),
|
||||
(Err(_), None) => (Directive::Note, "Note".to_owned()),
|
||||
(Ok(directive), Some(title)) => (directive, title),
|
||||
(Err(_), Some(title)) => (Directive::Note, title),
|
||||
};
|
||||
// If the user explicitly gave no title, then disable the title bar
|
||||
let title = if title.is_empty() { None } else { Some(title) };
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the first letter of `input` upppercase.
|
||||
///
|
||||
/// source: https://stackoverflow.com/a/38406885
|
||||
fn ucfirst(input: &str) -> String {
|
||||
let mut chars = input.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
Some(if let Ok(config) = v1::from_config_string(config_string) {
|
||||
// If we succeed at parsing v1, return that.
|
||||
Ok(config)
|
||||
} else {
|
||||
// Otherwise return our v2 error.
|
||||
Err(config_v2_error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,48 +59,30 @@ mod test {
|
||||
#[test]
|
||||
fn test_from_info_string() {
|
||||
// Not admonition blocks
|
||||
assert_eq!(AdmonitionInfoRaw::from_info_string(""), None);
|
||||
assert_eq!(AdmonitionInfoRaw::from_info_string("adm"), None);
|
||||
assert_eq!(InstanceConfig::from_info_string(""), None);
|
||||
assert_eq!(InstanceConfig::from_info_string("adm"), None);
|
||||
// v1 syntax is supported back compatibly
|
||||
assert_eq!(
|
||||
AdmonitionInfoRaw::from_info_string("admonish note.additional-classname")
|
||||
InstanceConfig::from_info_string("admonish note.additional-classname")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: vec!["additional-classname".to_owned()],
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
// v2 syntax is supported
|
||||
assert_eq!(
|
||||
AdmonitionInfoRaw::from_info_string(r#"admonish title="Custom Title" type="question""#)
|
||||
InstanceConfig::from_info_string(r#"admonish title="Custom Title" type="question""#)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "question".to_owned(),
|
||||
title: Some("Custom Title".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw() {
|
||||
assert_eq!(
|
||||
AdmonitionInfo::from(AdmonitionInfoRaw {
|
||||
directive: " ".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}),
|
||||
AdmonitionInfo {
|
||||
directive: Directive::Note,
|
||||
title: Some("Note".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::AdmonitionInfoRaw;
|
||||
use super::InstanceConfig;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
pub(crate) fn from_config_string(config_string: &str) -> Result<AdmonitionInfoRaw, String> {
|
||||
pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig, String> {
|
||||
let config_string = config_string.trim();
|
||||
|
||||
static RX_CONFIG_STRING_V1: Lazy<Regex> = Lazy::new(|| {
|
||||
@@ -49,11 +49,11 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<AdmonitionInfoRa
|
||||
),
|
||||
};
|
||||
|
||||
Ok(AdmonitionInfoRaw {
|
||||
Ok(InstanceConfig {
|
||||
directive: directive.to_owned(),
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,47 +66,47 @@ mod test {
|
||||
fn test_from_config_string() {
|
||||
assert_eq!(
|
||||
from_config_string("").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(" ").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("unknown").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "unknown".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("note").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("note.additional-classname").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: vec!["additional-classname".to_owned()],
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::AdmonitionInfoRaw;
|
||||
use super::InstanceConfig;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct AdmonitionInfoConfig {
|
||||
struct UserInput {
|
||||
#[serde(default)]
|
||||
r#type: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -12,7 +12,7 @@ struct AdmonitionInfoConfig {
|
||||
#[serde(default)]
|
||||
class: Option<String>,
|
||||
#[serde(default)]
|
||||
collapsible: bool,
|
||||
collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
/// Transform our config string into valid toml
|
||||
@@ -43,11 +43,11 @@ fn bare_key_value_pairs_to_toml(pairs: &str) -> String {
|
||||
///
|
||||
/// Note that if an error occurs, a parsed struct that can be returned to
|
||||
/// show the error message will be returned.
|
||||
pub(crate) fn from_config_string(config_string: &str) -> Result<AdmonitionInfoRaw, String> {
|
||||
pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig, String> {
|
||||
let config_toml = bare_key_value_pairs_to_toml(config_string);
|
||||
let config_toml = config_toml.trim();
|
||||
|
||||
let config: AdmonitionInfoConfig = match toml::from_str(config_toml) {
|
||||
let config: UserInput = match toml::from_str(config_toml) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
let original_error = Err(format!("TOML parsing error: {error}"));
|
||||
@@ -67,7 +67,7 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<AdmonitionInfoRa
|
||||
return original_error;
|
||||
}
|
||||
|
||||
let mut config: AdmonitionInfoConfig = match toml::from_str(config_toml) {
|
||||
let mut config: UserInput = match toml::from_str(config_toml) {
|
||||
Ok(config) => config,
|
||||
Err(_) => return original_error,
|
||||
};
|
||||
@@ -85,7 +85,7 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<AdmonitionInfoRa
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Ok(AdmonitionInfoRaw {
|
||||
Ok(InstanceConfig {
|
||||
directive: config.r#type.unwrap_or_default(),
|
||||
title: config.title,
|
||||
additional_classnames,
|
||||
@@ -102,60 +102,62 @@ mod test {
|
||||
fn test_from_config_string_v2() {
|
||||
assert_eq!(
|
||||
from_config_string("").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(" ").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(r#"type="note" class="additional classname" title="Никита""#)
|
||||
.unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
from_config_string(
|
||||
r#"type="note" class="additional classname" title="Никита" collapsible=true"#
|
||||
)
|
||||
.unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: Some("Никита".to_owned()),
|
||||
additional_classnames: vec!["additional".to_owned(), "classname".to_owned()],
|
||||
collapsible: false,
|
||||
collapsible: Some(true),
|
||||
}
|
||||
);
|
||||
// Specifying unknown keys is okay, as long as they're valid
|
||||
assert_eq!(
|
||||
from_config_string(r#"unkonwn="but valid toml""#).unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
// Just directive is fine
|
||||
assert_eq!(
|
||||
from_config_string(r#"info"#).unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
// Directive plus toml config
|
||||
assert_eq!(
|
||||
from_config_string(r#"info title="Information""#).unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
from_config_string(r#"info title="Information" collapsible=false"#).unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("Information".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
collapsible: Some(false),
|
||||
}
|
||||
);
|
||||
// Directive after toml config is an error
|
||||
@@ -166,7 +168,12 @@ mod test {
|
||||
fn test_from_config_string_invalid_toml_value() {
|
||||
assert_eq!(
|
||||
from_config_string(r#"note titlel=""#).unwrap_err(),
|
||||
"TOML parsing error: expected an equals, found a newline at line 1 column 6".to_owned()
|
||||
r#"TOML parsing error: TOML parse error at line 1, column 6
|
||||
|
|
||||
1 | note
|
||||
| ^
|
||||
expected `.`, `=`
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
785
src/lib.rs
785
src/lib.rs
@@ -1,781 +1,10 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use mdbook::{
|
||||
book::{Book, BookItem},
|
||||
errors::Result as MdbookResult,
|
||||
preprocess::{Preprocessor, PreprocessorContext},
|
||||
utils::unique_id_from_content,
|
||||
};
|
||||
use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
|
||||
mod book_config;
|
||||
mod config;
|
||||
mod markdown;
|
||||
mod parse;
|
||||
mod preprocessor;
|
||||
mod render;
|
||||
mod resolve;
|
||||
mod types;
|
||||
|
||||
use crate::{config::AdmonitionInfo, types::Directive};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum OnFailure {
|
||||
Bail,
|
||||
Continue,
|
||||
}
|
||||
|
||||
impl Default for OnFailure {
|
||||
fn default() -> Self {
|
||||
Self::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for OnFailure {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(string: &str) -> Result<Self, ()> {
|
||||
match string {
|
||||
"bail" => Ok(Self::Bail),
|
||||
"continue" => Ok(Self::Continue),
|
||||
_ => Ok(Self::Continue),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OnFailure {
|
||||
fn from_context(context: &PreprocessorContext) -> Self {
|
||||
context
|
||||
.config
|
||||
.get("preprocessor.admonish.on_failure")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(|value| OnFailure::from_str(value).unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Admonish;
|
||||
|
||||
impl Preprocessor for Admonish {
|
||||
fn name(&self) -> &str {
|
||||
"admonish"
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> MdbookResult<Book> {
|
||||
ensure_compatible_assets_version(ctx)?;
|
||||
let on_failure = OnFailure::from_context(ctx);
|
||||
|
||||
let mut res = None;
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let Some(Err(_)) = res {
|
||||
return;
|
||||
}
|
||||
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
res = Some(preprocess(&chapter.content, on_failure).map(|md| {
|
||||
chapter.content = md;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
res.unwrap_or(Ok(())).map(|_| book)
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
renderer == "html"
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_compatible_assets_version(ctx: &PreprocessorContext) -> Result<()> {
|
||||
use semver::{Version, VersionReq};
|
||||
|
||||
const REQUIRES_ASSETS_VERSION: &str = std::include_str!("./REQUIRED_ASSETS_VERSION");
|
||||
let requirement = VersionReq::parse(REQUIRES_ASSETS_VERSION.trim()).unwrap();
|
||||
|
||||
const USER_ACTION: &str = "Please run `mdbook-admonish install` to update installed assets.";
|
||||
const DOCS_REFERENCE: &str = "For more information, see: https://github.com/tommilligan/mdbook-admonish#semantic-versioning";
|
||||
|
||||
let version = match ctx
|
||||
.config
|
||||
.get("preprocessor.admonish.assets_version")
|
||||
.and_then(|value| value.as_str())
|
||||
{
|
||||
Some(version) => version,
|
||||
None => {
|
||||
return Err(anyhow!(
|
||||
r#"ERROR:
|
||||
Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but did not find a version.
|
||||
{USER_ACTION}
|
||||
{DOCS_REFERENCE}"#
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let version = Version::parse(version).unwrap();
|
||||
|
||||
if !requirement.matches(&version) {
|
||||
return Err(anyhow!(
|
||||
r#"ERROR:
|
||||
Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but found '{version}'.
|
||||
{USER_ACTION}
|
||||
{DOCS_REFERENCE}"#
|
||||
));
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Directive {
|
||||
fn classname(&self) -> &'static str {
|
||||
match self {
|
||||
Directive::Note => "note",
|
||||
Directive::Abstract => "abstract",
|
||||
Directive::Info => "info",
|
||||
Directive::Tip => "tip",
|
||||
Directive::Success => "success",
|
||||
Directive::Question => "question",
|
||||
Directive::Warning => "warning",
|
||||
Directive::Failure => "failure",
|
||||
Directive::Danger => "danger",
|
||||
Directive::Bug => "bug",
|
||||
Directive::Example => "example",
|
||||
Directive::Quote => "quote",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Admonition<'a> {
|
||||
directive: Directive,
|
||||
title: Option<String>,
|
||||
content: Cow<'a, str>,
|
||||
additional_classnames: Vec<String>,
|
||||
collapsible: bool,
|
||||
}
|
||||
|
||||
impl<'a> Admonition<'a> {
|
||||
pub fn new(info: AdmonitionInfo, content: &'a str) -> Self {
|
||||
let AdmonitionInfo {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
} = info;
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
content: Cow::Borrowed(content),
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
}
|
||||
}
|
||||
|
||||
fn html(&self, anchor_id: &str) -> String {
|
||||
let mut additional_class = Cow::Borrowed(self.directive.classname());
|
||||
let title = &self.title;
|
||||
let content = &self.content;
|
||||
|
||||
let title_block = if self.collapsible { "summary" } else { "div" };
|
||||
|
||||
let title_html = title
|
||||
.as_ref()
|
||||
.map(|title| {
|
||||
Cow::Owned(format!(
|
||||
r##"<{title_block} class="admonition-title">
|
||||
|
||||
{title}
|
||||
|
||||
<a class="admonition-anchor-link" href="#{ANCHOR_ID_PREFIX}-{anchor_id}"></a>
|
||||
</{title_block}>
|
||||
"##
|
||||
))
|
||||
})
|
||||
.unwrap_or(Cow::Borrowed(""));
|
||||
|
||||
if !self.additional_classnames.is_empty() {
|
||||
let mut buffer = additional_class.into_owned();
|
||||
for additional_classname in &self.additional_classnames {
|
||||
buffer.push(' ');
|
||||
buffer.push_str(additional_classname);
|
||||
}
|
||||
|
||||
additional_class = Cow::Owned(buffer);
|
||||
}
|
||||
|
||||
let admonition_block = if self.collapsible { "details" } else { "div" };
|
||||
// Notes on the HTML template:
|
||||
// - the additional whitespace around the content are deliberate
|
||||
// In line with the commonmark spec, this allows the inner content to be
|
||||
// rendered as markdown paragraphs.
|
||||
format!(
|
||||
r#"<{admonition_block} id="{ANCHOR_ID_PREFIX}-{anchor_id}" class="admonition {additional_class}">
|
||||
{title_html}<div>
|
||||
|
||||
{content}
|
||||
|
||||
</div>
|
||||
</{admonition_block}>"#,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ANCHOR_ID_PREFIX: &str = "admonition";
|
||||
const ANCHOR_ID_DEFAULT: &str = "default";
|
||||
|
||||
fn extract_admonish_body(content: &str) -> &str {
|
||||
const PRE_END: char = '\n';
|
||||
const POST: &str = "```";
|
||||
|
||||
// We can't trust the info string length to find the start of the body
|
||||
// it may change length if it contains HTML or character escapes.
|
||||
//
|
||||
// So we scan for the first newline and use that.
|
||||
// If gods forbid it doesn't exist for some reason, just include the whole info string.
|
||||
let start_index = content
|
||||
// Start one character _after_ the newline
|
||||
.find(PRE_END)
|
||||
.map(|index| index + 1)
|
||||
.unwrap_or_default();
|
||||
let end_index = content.len() - POST.len();
|
||||
|
||||
let admonish_content = &content[start_index..end_index];
|
||||
// The newline after a code block is technically optional, so we have to
|
||||
// trim it off dynamically.
|
||||
admonish_content.trim()
|
||||
}
|
||||
|
||||
/// Given the content in the span of the code block, and the info string,
|
||||
/// return `Some(Admonition)` if the code block is an admonition.
|
||||
///
|
||||
/// If there is an error parsing the admonition, either:
|
||||
///
|
||||
/// - Display a UI error message output in the book.
|
||||
/// - If configured, break the build.
|
||||
///
|
||||
/// If the code block is not an admonition, return `None`.
|
||||
fn parse_admonition<'a>(
|
||||
info_string: &'a str,
|
||||
content: &'a str,
|
||||
on_failure: OnFailure,
|
||||
) -> Option<MdbookResult<Admonition<'a>>> {
|
||||
let info = AdmonitionInfo::from_info_string(info_string)?;
|
||||
let info = match info {
|
||||
Ok(info) => info,
|
||||
// FIXME return error messages to break build if configured
|
||||
// Err(message) => return Some(Err(content)),
|
||||
Err(message) => {
|
||||
return Some(match on_failure {
|
||||
OnFailure::Continue => Ok(Admonition {
|
||||
directive: Directive::Bug,
|
||||
title: Some("Error rendering admonishment".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
content: Cow::Owned(format!(
|
||||
r#"Failed with: {message}
|
||||
|
||||
Original markdown input:
|
||||
|
||||
``````
|
||||
{content}
|
||||
``````
|
||||
"#
|
||||
)),
|
||||
}),
|
||||
OnFailure::Bail => Err(anyhow!("Error processing admonition, bailing:\n{content}")),
|
||||
})
|
||||
}
|
||||
};
|
||||
let body = extract_admonish_body(content);
|
||||
Some(Ok(Admonition::new(info, body)))
|
||||
}
|
||||
|
||||
fn preprocess(content: &str, on_failure: OnFailure) -> MdbookResult<String> {
|
||||
let mut id_counter = Default::default();
|
||||
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);
|
||||
|
||||
let mut admonish_blocks = vec![];
|
||||
|
||||
let events = Parser::new_ext(content, opts);
|
||||
for (e, span) in events.into_offset_iter() {
|
||||
if let Event::Start(Tag::CodeBlock(Fenced(info_string))) = e.clone() {
|
||||
let span_content = &content[span.start..span.end];
|
||||
let admonition = match parse_admonition(info_string.as_ref(), span_content, on_failure)
|
||||
{
|
||||
Some(admonition) => admonition,
|
||||
None => continue,
|
||||
};
|
||||
let admonition = admonition?;
|
||||
let anchor_id = unique_id_from_content(
|
||||
admonition.title.as_deref().unwrap_or(ANCHOR_ID_DEFAULT),
|
||||
&mut id_counter,
|
||||
);
|
||||
admonish_blocks.push((span, admonition.html(&anchor_id)));
|
||||
}
|
||||
}
|
||||
|
||||
let mut content = content.to_string();
|
||||
for (span, block) in admonish_blocks.iter().rev() {
|
||||
let pre_content = &content[..span.start];
|
||||
let post_content = &content[span.end..];
|
||||
content = format!("{}\n{}{}", pre_content, block, post_content);
|
||||
}
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn prep(content: &str) -> String {
|
||||
preprocess(content, OnFailure::Continue).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish() {
|
||||
let content = r#"# Chapter
|
||||
```admonish
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive() {
|
||||
let content = r#"# Chapter
|
||||
```admonish warning
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-warning" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Warning
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-warning"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive_title() {
|
||||
let content = r#"# Chapter
|
||||
```admonish warning "Read **this**!"
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-read-this" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Read **this**!
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-read-this"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_tables_untouched() {
|
||||
// Regression test.
|
||||
// Previously we forgot to enable the same markdwon extensions as mdbook itself.
|
||||
|
||||
let content = r#"# Heading
|
||||
| Head 1 | Head 2 |
|
||||
|--------|--------|
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
| Head 1 | Head 2 |
|
||||
|--------|--------|
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_html_untouched() {
|
||||
// Regression test.
|
||||
// Don't remove important newlines for syntax nested inside HTML
|
||||
|
||||
let content = r#"# Heading
|
||||
<del>
|
||||
*foo*
|
||||
</del>
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
<del>
|
||||
*foo*
|
||||
</del>
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_in_list() {
|
||||
// Regression test.
|
||||
// Don't remove important newlines for syntax nested inside HTML
|
||||
|
||||
let content = r#"# Heading
|
||||
1. paragraph 1
|
||||
```
|
||||
code 1
|
||||
```
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
1. paragraph 1
|
||||
```
|
||||
code 1
|
||||
```
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_string_that_changes_length_when_parsed() {
|
||||
let content = r#"
|
||||
```admonish note "And \\"<i>in</i>\\" the title"
|
||||
With <b>html</b> styling.
|
||||
```
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-and-in-the-title" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
And "<i>in</i>" the title
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-and-in-the-title"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
With <b>html</b> styling.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
hello
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_string_ending_in_symbol() {
|
||||
let content = r#"
|
||||
```admonish warning "Trademark™"
|
||||
Should be respected
|
||||
```
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-trademark" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Trademark™
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-trademark"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Should be respected
|
||||
|
||||
</div>
|
||||
</div>
|
||||
hello
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_additional_classname() {
|
||||
let content = r#"
|
||||
```admonish tip.my-style.other-style
|
||||
Will have bonus classnames
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-tip" class="admonition tip my-style other-style">
|
||||
<div class="admonition-title">
|
||||
|
||||
Tip
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-tip"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Will have bonus classnames
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_additional_classname_and_title() {
|
||||
let content = r#"
|
||||
```admonish tip.my-style.other-style "Developers don't want you to know this one weird tip!"
|
||||
Will have bonus classnames
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-developers-dont-want-you-to-know-this-one-weird-tip" class="admonition tip my-style other-style">
|
||||
<div class="admonition-title">
|
||||
|
||||
Developers don't want you to know this one weird tip!
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-developers-dont-want-you-to-know-this-one-weird-tip"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Will have bonus classnames
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_empty_additional_classnames_title_content() {
|
||||
let content = r#"
|
||||
```admonish .... ""
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-default" class="admonition note">
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unique_ids_same_title() {
|
||||
let content = r#"
|
||||
```admonish note "My Note"
|
||||
Content zero.
|
||||
```
|
||||
|
||||
```admonish note "My Note"
|
||||
Content one.
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-my-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
My Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-my-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Content zero.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="admonition-my-note-1" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
My Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-my-note-1"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Content one.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_config_works() {
|
||||
let content = r#"
|
||||
```admonish tip class="my other-style" title="Article Heading"
|
||||
Bonus content!
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-article-heading" class="admonition tip my other-style">
|
||||
<div class="admonition-title">
|
||||
|
||||
Article Heading
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-article-heading"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Bonus content!
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continue_on_error_output() {
|
||||
let content = r#"
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-error-rendering-admonishment" class="admonition bug">
|
||||
<div class="admonition-title">
|
||||
|
||||
Error rendering admonishment
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Failed with: TOML parsing error: unterminated string at line 1 column 7
|
||||
|
||||
Original markdown input:
|
||||
|
||||
``````
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```
|
||||
``````
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bail_on_error_output() {
|
||||
let content = r#"
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```
|
||||
"#;
|
||||
|
||||
assert_eq!(
|
||||
preprocess(content, OnFailure::Bail)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
r#"Error processing admonition, bailing:
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```"#
|
||||
.to_owned()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_collapsible() {
|
||||
let content = r#"
|
||||
```admonish collapsible=true
|
||||
Hidden
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<details id="admonition-note" class="admonition note">
|
||||
<summary class="admonition-title">
|
||||
|
||||
Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</summary>
|
||||
<div>
|
||||
|
||||
Hidden
|
||||
|
||||
</div>
|
||||
</details>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
}
|
||||
pub use crate::preprocessor::Admonish;
|
||||
|
||||
806
src/markdown.rs
Normal file
806
src/markdown.rs
Normal file
@@ -0,0 +1,806 @@
|
||||
use mdbook::errors::Result as MdbookResult;
|
||||
use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag};
|
||||
|
||||
pub use crate::preprocessor::Admonish;
|
||||
use crate::{
|
||||
book_config::OnFailure,
|
||||
parse::parse_admonition,
|
||||
types::{AdmonitionDefaults, RenderTextMode},
|
||||
};
|
||||
|
||||
pub(crate) fn preprocess(
|
||||
content: &str,
|
||||
on_failure: OnFailure,
|
||||
admonition_defaults: &AdmonitionDefaults,
|
||||
render_text_mode: RenderTextMode,
|
||||
) -> MdbookResult<String> {
|
||||
let mut id_counter = Default::default();
|
||||
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);
|
||||
|
||||
let mut admonish_blocks = vec![];
|
||||
|
||||
let events = Parser::new_ext(content, opts);
|
||||
|
||||
for (event, span) in events.into_offset_iter() {
|
||||
if let Event::Start(Tag::CodeBlock(Fenced(info_string))) = event.clone() {
|
||||
let span_content = &content[span.start..span.end];
|
||||
|
||||
// Scan for a line start before this span.
|
||||
// For safety, only scan up to a fixed limit of the text
|
||||
const INDENT_SCAN_MAX: usize = 1024;
|
||||
// If there's less text than that, just scan from the start
|
||||
let line_scan_start = span.start.checked_sub(INDENT_SCAN_MAX).unwrap_or_default();
|
||||
// If we can't find a newline, assume no indent
|
||||
let indent = content[line_scan_start..span.start]
|
||||
.chars()
|
||||
.rev()
|
||||
.position(|c| c == '\n')
|
||||
.unwrap_or_default();
|
||||
|
||||
let admonition = match parse_admonition(
|
||||
info_string.as_ref(),
|
||||
admonition_defaults,
|
||||
span_content,
|
||||
on_failure,
|
||||
indent,
|
||||
) {
|
||||
Some(admonition) => admonition,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let admonition = admonition?;
|
||||
|
||||
// Once we've identitified admonition blocks, handle them differently
|
||||
// depending on our render mode
|
||||
let new_content = match render_text_mode {
|
||||
RenderTextMode::Html => admonition.html_with_unique_ids(&mut id_counter),
|
||||
RenderTextMode::Strip => admonition.strip(),
|
||||
};
|
||||
|
||||
admonish_blocks.push((span, new_content));
|
||||
}
|
||||
}
|
||||
|
||||
let mut content = content.to_string();
|
||||
for (span, block) in admonish_blocks.iter().rev() {
|
||||
let pre_content = &content[..span.start];
|
||||
let post_content = &content[span.end..];
|
||||
content = format!("{}{}{}", pre_content, block, post_content);
|
||||
}
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn prep(content: &str) -> String {
|
||||
preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults::default(),
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish() {
|
||||
let content = r#"# Chapter
|
||||
```admonish
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_longer_code_fence() {
|
||||
let content = r#"# Chapter
|
||||
````admonish
|
||||
```json
|
||||
{}
|
||||
```
|
||||
````
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive() {
|
||||
let content = r#"# Chapter
|
||||
```admonish warning
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-warning" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Warning
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-warning"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive_alternate() {
|
||||
let content = r#"# Chapter
|
||||
```admonish caution
|
||||
A warning with alternate title.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-caution" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Caution
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-caution"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A warning with alternate title.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive_title() {
|
||||
let content = r#"# Chapter
|
||||
```admonish warning "Read **this**!"
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-read-this" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Read **this**!
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-read-this"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_tables_untouched() {
|
||||
// Regression test.
|
||||
// Previously we forgot to enable the same markdwon extensions as mdbook itself.
|
||||
|
||||
let content = r#"# Heading
|
||||
| Head 1 | Head 2 |
|
||||
|--------|--------|
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
| Head 1 | Head 2 |
|
||||
|--------|--------|
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_html_untouched() {
|
||||
// Regression test.
|
||||
// Don't remove important newlines for syntax nested inside HTML
|
||||
|
||||
let content = r#"# Heading
|
||||
<del>
|
||||
*foo*
|
||||
</del>
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
<del>
|
||||
*foo*
|
||||
</del>
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_in_list() {
|
||||
// Regression test.
|
||||
// Don't remove important newlines for syntax nested inside HTML
|
||||
|
||||
let content = r#"# Heading
|
||||
1. paragraph 1
|
||||
```
|
||||
code 1
|
||||
```
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
1. paragraph 1
|
||||
```
|
||||
code 1
|
||||
```
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_string_that_changes_length_when_parsed() {
|
||||
let content = r#"
|
||||
```admonish note "And \\"<i>in</i>\\" the title"
|
||||
With <b>html</b> styling.
|
||||
```
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-and-in-the-title" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
And "<i>in</i>" the title
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-and-in-the-title"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
With <b>html</b> styling.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
hello
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_string_ending_in_symbol() {
|
||||
let content = r#"
|
||||
```admonish warning "Trademark™"
|
||||
Should be respected
|
||||
```
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-trademark" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Trademark™
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-trademark"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Should be respected
|
||||
|
||||
</div>
|
||||
</div>
|
||||
hello
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_additional_classname() {
|
||||
let content = r#"
|
||||
```admonish tip.my-style.other-style
|
||||
Will have bonus classnames
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-tip" class="admonition tip my-style other-style">
|
||||
<div class="admonition-title">
|
||||
|
||||
Tip
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-tip"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Will have bonus classnames
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_additional_classname_and_title() {
|
||||
let content = r#"
|
||||
```admonish tip.my-style.other-style "Developers don't want you to know this one weird tip!"
|
||||
Will have bonus classnames
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-developers-dont-want-you-to-know-this-one-weird-tip" class="admonition tip my-style other-style">
|
||||
<div class="admonition-title">
|
||||
|
||||
Developers don't want you to know this one weird tip!
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-developers-dont-want-you-to-know-this-one-weird-tip"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Will have bonus classnames
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_empty_additional_classnames_title_content() {
|
||||
let content = r#"
|
||||
```admonish .... ""
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
|
||||
<div id="admonition-default" class="admonition note">
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unique_ids_same_title() {
|
||||
let content = r#"
|
||||
```admonish note "My Note"
|
||||
Content zero.
|
||||
```
|
||||
|
||||
```admonish note "My Note"
|
||||
Content one.
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-my-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
My Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-my-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Content zero.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="admonition-my-note-1" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
My Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-my-note-1"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Content one.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_config_works() {
|
||||
let content = r#"
|
||||
```admonish tip class="my other-style" title="Article Heading"
|
||||
Bonus content!
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-article-heading" class="admonition tip my other-style">
|
||||
<div class="admonition-title">
|
||||
|
||||
Article Heading
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-article-heading"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Bonus content!
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continue_on_error_output() {
|
||||
let content = r#"
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-error-rendering-admonishment" class="admonition bug">
|
||||
<div class="admonition-title">
|
||||
|
||||
Error rendering admonishment
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Failed with:
|
||||
|
||||
```log
|
||||
TOML parsing error: TOML parse error at line 1, column 8
|
||||
|
|
||||
1 | title="
|
||||
| ^
|
||||
invalid basic string
|
||||
|
||||
```
|
||||
|
||||
Original markdown input:
|
||||
|
||||
````markdown
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bail_on_error_output() {
|
||||
let content = r#"
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```
|
||||
"#;
|
||||
assert_eq!(
|
||||
preprocess(
|
||||
content,
|
||||
OnFailure::Bail,
|
||||
&AdmonitionDefaults::default(),
|
||||
RenderTextMode::Html
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
r#"Error processing admonition, bailing:
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```"#
|
||||
.to_owned()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renderer_strip_explicit() {
|
||||
let content = r#"
|
||||
````admonish title="Title"
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
````
|
||||
"#;
|
||||
assert_eq!(
|
||||
preprocess(
|
||||
content,
|
||||
OnFailure::Bail,
|
||||
&AdmonitionDefaults::default(),
|
||||
RenderTextMode::Strip
|
||||
)
|
||||
.unwrap(),
|
||||
r#"
|
||||
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
|
||||
"#
|
||||
.to_owned()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_collapsible() {
|
||||
let content = r#"
|
||||
```admonish collapsible=true
|
||||
Hidden
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<details id="admonition-note" class="admonition note">
|
||||
<summary class="admonition-title">
|
||||
|
||||
Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</summary>
|
||||
<div>
|
||||
|
||||
Hidden
|
||||
|
||||
</div>
|
||||
</details>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_toml_title() {
|
||||
let content = r#"# Chapter
|
||||
```admonish
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-admonish" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
Admonish
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-admonish"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
let preprocess_result = preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Admonish".to_owned()),
|
||||
collapsible: false,
|
||||
},
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(expected, preprocess_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_explicit_title_with_default() {
|
||||
let content = r#"# Chapter
|
||||
```admonish title=""
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r#"# Chapter
|
||||
|
||||
<div id="admonition-default" class="admonition note">
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"#;
|
||||
|
||||
let preprocess_result = preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Admonish".to_owned()),
|
||||
collapsible: false,
|
||||
},
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(expected, preprocess_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_explicit_title() {
|
||||
let content = r#"# Chapter
|
||||
```admonish title=""
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r#"# Chapter
|
||||
|
||||
<div id="admonition-default" class="admonition note">
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_embed() {
|
||||
let content = r#"# Chapter
|
||||
|
||||
1. Thing one
|
||||
|
||||
```sh
|
||||
Thing one
|
||||
```
|
||||
|
||||
1. Thing two
|
||||
|
||||
```admonish
|
||||
Thing two
|
||||
```
|
||||
|
||||
1. Thing three
|
||||
|
||||
```sh
|
||||
Thing three
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
1. Thing one
|
||||
|
||||
```sh
|
||||
Thing one
|
||||
```
|
||||
|
||||
1. Thing two
|
||||
|
||||
|
||||
<div id="admonition-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Thing two
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
1. Thing three
|
||||
|
||||
```sh
|
||||
Thing three
|
||||
```
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
}
|
||||
240
src/parse.rs
Normal file
240
src/parse.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::preprocessor::Admonish;
|
||||
use crate::{
|
||||
book_config::OnFailure,
|
||||
render::Admonition,
|
||||
resolve::AdmonitionMeta,
|
||||
types::{AdmonitionDefaults, Directive},
|
||||
};
|
||||
|
||||
/// Given the content in the span of the code block, and the info string,
|
||||
/// return `Some(Admonition)` if the code block is an admonition.
|
||||
///
|
||||
/// If there is an error parsing the admonition, either:
|
||||
///
|
||||
/// - Display a UI error message output in the book.
|
||||
/// - If configured, break the build.
|
||||
///
|
||||
/// If the code block is not an admonition, return `None`.
|
||||
pub(crate) fn parse_admonition<'a>(
|
||||
info_string: &'a str,
|
||||
admonition_defaults: &'a AdmonitionDefaults,
|
||||
content: &'a str,
|
||||
on_failure: OnFailure,
|
||||
indent: usize,
|
||||
) -> Option<Result<Admonition<'a>>> {
|
||||
// We need to know fence details anyway for error messages
|
||||
let extracted = extract_admonish_body(content);
|
||||
|
||||
let info = AdmonitionMeta::from_info_string(info_string, admonition_defaults)?;
|
||||
let info = match info {
|
||||
Ok(info) => info,
|
||||
Err(message) => {
|
||||
// Construct a fence capable of enclosing whatever we wrote for the
|
||||
// actual input block
|
||||
let fence = extracted.fence;
|
||||
let enclosing_fence: String = std::iter::repeat(fence.character)
|
||||
.take(fence.length + 1)
|
||||
.collect();
|
||||
return Some(match on_failure {
|
||||
OnFailure::Continue => {
|
||||
log::warn!(
|
||||
r#"Error processing admonition. To fail the build instead of continuing, set 'on_failure = "bail"'"#
|
||||
);
|
||||
Ok(Admonition {
|
||||
directive: Directive::Bug,
|
||||
title: "Error rendering admonishment".to_owned(),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
content: Cow::Owned(format!(
|
||||
r#"Failed with:
|
||||
|
||||
```log
|
||||
{message}
|
||||
```
|
||||
|
||||
Original markdown input:
|
||||
|
||||
{enclosing_fence}markdown
|
||||
{content}
|
||||
{enclosing_fence}
|
||||
"#
|
||||
)),
|
||||
indent,
|
||||
})
|
||||
}
|
||||
OnFailure::Bail => Err(anyhow!("Error processing admonition, bailing:\n{content}")),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Some(Ok(Admonition::new(
|
||||
info,
|
||||
extracted.body,
|
||||
// Note that this is a bit hacky - the fence information comes from the start
|
||||
// of the block, and includes the whole line.
|
||||
//
|
||||
// This is more likely to be what we want, as ending indentation is unrelated
|
||||
// according to the commonmark spec (ref https://spec.commonmark.org/0.12/#example-85)
|
||||
//
|
||||
// The main case we're worried about here is indenting enough to be inside list items,
|
||||
// and in this case the starting code fence must be indented enough to be considered
|
||||
// part of the list item.
|
||||
//
|
||||
// The hacky thing is that we're considering line indent in the document as a whole,
|
||||
// not relative to the context of some containing item. But I think that's what we
|
||||
// want for now, anyway.
|
||||
indent,
|
||||
)))
|
||||
}
|
||||
|
||||
/// We can't trust the info string length to find the start of the body
|
||||
/// it may change length if it contains HTML or character escapes.
|
||||
///
|
||||
/// So we scan for the first newline and use that.
|
||||
/// If gods forbid it doesn't exist for some reason, just include the whole info string.
|
||||
fn extract_admonish_body_start_index(content: &str) -> usize {
|
||||
let index = content
|
||||
.find('\n')
|
||||
// Start one character _after_ the newline
|
||||
.map(|index| index + 1);
|
||||
|
||||
// If we can't get a valid index, include all content
|
||||
match index {
|
||||
// Couldn't find a newline
|
||||
None => 0,
|
||||
Some(index) => {
|
||||
// Index out of bound of content
|
||||
if index > (content.len() - 1) {
|
||||
0
|
||||
} else {
|
||||
index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_admonish_body_end_index(content: &str) -> (usize, Fence) {
|
||||
let fence_character = content.chars().next_back().unwrap_or('`');
|
||||
let number_fence_characters = content
|
||||
.chars()
|
||||
.rev()
|
||||
.position(|c| c != fence_character)
|
||||
.unwrap_or_default();
|
||||
let fence = Fence::new(fence_character, number_fence_characters);
|
||||
|
||||
let index = content.len() - fence.length;
|
||||
(index, fence)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Fence {
|
||||
character: char,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl Fence {
|
||||
fn new(character: char, length: usize) -> Self {
|
||||
Self { character, length }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Extracted<'a> {
|
||||
body: &'a str,
|
||||
fence: Fence,
|
||||
}
|
||||
|
||||
/// Given the whole text content of the code fence, extract the body.
|
||||
///
|
||||
/// This really feels like we should get the markdown parser to do it for us,
|
||||
/// but it's not really clear a good way of doing that.
|
||||
///
|
||||
/// ref: https://spec.commonmark.org/0.30/#fenced-code-blocks
|
||||
fn extract_admonish_body(content: &str) -> Extracted<'_> {
|
||||
let start_index = extract_admonish_body_start_index(content);
|
||||
let (end_index, fence) = extract_admonish_body_end_index(content);
|
||||
|
||||
let admonish_content = &content[start_index..end_index];
|
||||
// The newline after a code block is technically optional, so we have to
|
||||
// trim it off dynamically.
|
||||
let body = admonish_content.trim_end();
|
||||
Extracted { body, fence }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_extract_start() {
|
||||
for (text, expected) in [
|
||||
("```sane example\ncontent```", 16),
|
||||
("~~~~~\nlonger fence", 6),
|
||||
// empty
|
||||
("```\n```", 4),
|
||||
// bounds check, should not index outside of content
|
||||
("```\n", 0),
|
||||
] {
|
||||
let actual = extract_admonish_body_start_index(text);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_end() {
|
||||
for (text, expected) in [
|
||||
("\n```", (1, Fence::new('`', 3))),
|
||||
// different lengths
|
||||
("\n``````", (1, Fence::new('`', 6))),
|
||||
("\n~~~~", (1, Fence::new('~', 4))),
|
||||
// whitespace before fence end
|
||||
("\n ```", (4, Fence::new('`', 3))),
|
||||
("content\n```", (8, Fence::new('`', 3))),
|
||||
] {
|
||||
let actual = extract_admonish_body_end_index(text);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract() {
|
||||
fn content_fence(body: &'static str, character: char, length: usize) -> Extracted<'static> {
|
||||
Extracted {
|
||||
body,
|
||||
fence: Fence::new(character, length),
|
||||
}
|
||||
}
|
||||
for (text, expected) in [
|
||||
// empty
|
||||
("```\n```", content_fence("", '`', 3)),
|
||||
// standard
|
||||
(
|
||||
"```admonish\ncontent\n```",
|
||||
content_fence("content", '`', 3),
|
||||
),
|
||||
// whitespace
|
||||
(
|
||||
"```admonish \n content \n ```",
|
||||
content_fence(" content", '`', 3),
|
||||
),
|
||||
// longer
|
||||
(
|
||||
"``````admonish\ncontent\n``````",
|
||||
content_fence("content", '`', 6),
|
||||
),
|
||||
// unequal
|
||||
(
|
||||
"~~~admonish\ncontent\n~~~~~",
|
||||
// longer (end) fence returned
|
||||
content_fence("content", '~', 5),
|
||||
),
|
||||
] {
|
||||
let actual = extract_admonish_body(text);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
259
src/preprocessor.rs
Normal file
259
src/preprocessor.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use mdbook::{
|
||||
book::{Book, BookItem},
|
||||
errors::Result as MdbookResult,
|
||||
preprocess::{Preprocessor, PreprocessorContext},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
book_config::{admonish_config_from_context, Config, RenderMode},
|
||||
markdown::preprocess,
|
||||
types::RenderTextMode,
|
||||
};
|
||||
|
||||
pub struct Admonish;
|
||||
|
||||
impl Preprocessor for Admonish {
|
||||
fn name(&self) -> &str {
|
||||
"admonish"
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> MdbookResult<Book> {
|
||||
let config = admonish_config_from_context(ctx)?;
|
||||
ensure_compatible_assets_version(&config)?;
|
||||
|
||||
let on_failure = config.on_failure;
|
||||
let admonition_defaults = config.default;
|
||||
|
||||
// Load what rendering we should do from config, falling back to a default
|
||||
let render_mode = config
|
||||
.renderer
|
||||
.get(&ctx.renderer)
|
||||
.and_then(|renderer| renderer.render_mode)
|
||||
.unwrap_or_else(|| {
|
||||
// By default only render html for the html renderer
|
||||
// For everything else, do nothing
|
||||
if &ctx.renderer == "html" {
|
||||
RenderMode::Html
|
||||
} else {
|
||||
RenderMode::Preserve
|
||||
}
|
||||
});
|
||||
let render_text_mode = match render_mode {
|
||||
RenderMode::Preserve => return Ok(book),
|
||||
RenderMode::Html => RenderTextMode::Html,
|
||||
RenderMode::Strip => RenderTextMode::Strip,
|
||||
};
|
||||
|
||||
let mut res = None;
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let Some(Err(_)) = res {
|
||||
return;
|
||||
}
|
||||
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
res = Some(
|
||||
preprocess(
|
||||
&chapter.content,
|
||||
on_failure,
|
||||
&admonition_defaults,
|
||||
render_text_mode,
|
||||
)
|
||||
.map(|md| {
|
||||
chapter.content = md;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
res.unwrap_or(Ok(())).map(|_| book)
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, _renderer: &str) -> bool {
|
||||
// We support all renderers, but will only actually take action
|
||||
// if configured to do so - or, if it's the html renderer
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_compatible_assets_version(config: &Config) -> Result<()> {
|
||||
use semver::{Version, VersionReq};
|
||||
|
||||
const REQUIRES_ASSETS_VERSION: &str = std::include_str!("./REQUIRED_ASSETS_VERSION");
|
||||
let requirement = VersionReq::parse(REQUIRES_ASSETS_VERSION.trim()).unwrap();
|
||||
|
||||
const USER_ACTION: &str = "Please run `mdbook-admonish install` to update installed assets.";
|
||||
const DOCS_REFERENCE: &str = "For more information, see: https://github.com/tommilligan/mdbook-admonish#semantic-versioning";
|
||||
|
||||
let version = match &config.assets_version {
|
||||
Some(version) => version,
|
||||
None => {
|
||||
return Err(anyhow!(
|
||||
r#"ERROR:
|
||||
Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but did not find a version.
|
||||
{USER_ACTION}
|
||||
{DOCS_REFERENCE}"#
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let version = Version::parse(version).unwrap();
|
||||
|
||||
if !requirement.matches(&version) {
|
||||
return Err(anyhow!(
|
||||
r#"ERROR:
|
||||
Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but found '{version}'.
|
||||
{USER_ACTION}
|
||||
{DOCS_REFERENCE}"#
|
||||
));
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn mock_book(content: &str) -> Book {
|
||||
serde_json::from_value(json!({
|
||||
"sections": [
|
||||
{
|
||||
"Chapter": {
|
||||
"name": "Chapter 1",
|
||||
"content": content,
|
||||
"number": [1],
|
||||
"sub_items": [],
|
||||
"path": "chapter_1.md",
|
||||
"source_path": "chapter_1.md",
|
||||
"parent_names": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"__non_exhaustive": null
|
||||
}))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn mock_context(admonish: &Value, renderer: &str) -> PreprocessorContext {
|
||||
let value = json!({
|
||||
"root": "/path/to/book",
|
||||
"config": {
|
||||
"book": {
|
||||
"authors": ["AUTHOR"],
|
||||
"language": "en",
|
||||
"multilingual": false,
|
||||
"src": "src",
|
||||
"title": "TITLE"
|
||||
},
|
||||
"preprocessor": {
|
||||
"admonish": admonish,
|
||||
}
|
||||
},
|
||||
"renderer": renderer,
|
||||
"mdbook_version": "0.4.21"
|
||||
});
|
||||
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_html() {
|
||||
let content = r#"
|
||||
````admonish title="Title"
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
````
|
||||
"#;
|
||||
let expected_content = r##"
|
||||
|
||||
<div id="admonition-title" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
Title
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-title"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
let ctx = mock_context(
|
||||
&json!({
|
||||
"assets_version": "2.0.0"
|
||||
}),
|
||||
"html",
|
||||
);
|
||||
let book = mock_book(content);
|
||||
let expected_book = mock_book(expected_content);
|
||||
|
||||
assert_eq!(Admonish.run(&ctx, book).unwrap(), expected_book)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_test_preserves_by_default() {
|
||||
let content = r#"
|
||||
````admonish title="Title"
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
````
|
||||
"#;
|
||||
let ctx = mock_context(
|
||||
&json!({
|
||||
"assets_version": "2.0.0"
|
||||
}),
|
||||
"test",
|
||||
);
|
||||
let book = mock_book(content);
|
||||
let expected_book = book.clone();
|
||||
|
||||
assert_eq!(Admonish.run(&ctx, book).unwrap(), expected_book)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_test_can_strip() {
|
||||
let content = r#"
|
||||
````admonish title="Title"
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
````
|
||||
"#;
|
||||
let expected_content = r#"
|
||||
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
|
||||
"#;
|
||||
let ctx = mock_context(
|
||||
&json!({
|
||||
"assets_version": "2.0.0",
|
||||
"renderer": {
|
||||
"test": {
|
||||
"render_mode": "strip",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"test",
|
||||
);
|
||||
let book = mock_book(content);
|
||||
let expected_book = mock_book(expected_content);
|
||||
|
||||
assert_eq!(Admonish.run(&ctx, book).unwrap(), expected_book)
|
||||
}
|
||||
}
|
||||
125
src/render.rs
Normal file
125
src/render.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use mdbook::utils::unique_id_from_content;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use crate::preprocessor::Admonish;
|
||||
use crate::{resolve::AdmonitionMeta, types::Directive};
|
||||
|
||||
impl Directive {
|
||||
fn classname(&self) -> &'static str {
|
||||
match self {
|
||||
Directive::Note => "note",
|
||||
Directive::Abstract => "abstract",
|
||||
Directive::Info => "info",
|
||||
Directive::Tip => "tip",
|
||||
Directive::Success => "success",
|
||||
Directive::Question => "question",
|
||||
Directive::Warning => "warning",
|
||||
Directive::Failure => "failure",
|
||||
Directive::Danger => "danger",
|
||||
Directive::Bug => "bug",
|
||||
Directive::Example => "example",
|
||||
Directive::Quote => "quote",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Admonition<'a> {
|
||||
pub(crate) directive: Directive,
|
||||
pub(crate) title: String,
|
||||
pub(crate) content: Cow<'a, str>,
|
||||
pub(crate) additional_classnames: Vec<String>,
|
||||
pub(crate) collapsible: bool,
|
||||
pub(crate) indent: usize,
|
||||
}
|
||||
|
||||
impl<'a> Admonition<'a> {
|
||||
pub(crate) fn new(info: AdmonitionMeta, content: &'a str, indent: usize) -> Self {
|
||||
let AdmonitionMeta {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
} = info;
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
content: Cow::Borrowed(content),
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
indent,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn html_with_unique_ids(&self, id_counter: &mut HashMap<String, usize>) -> String {
|
||||
let anchor_id = unique_id_from_content(
|
||||
if !self.title.is_empty() {
|
||||
&self.title
|
||||
} else {
|
||||
ANCHOR_ID_DEFAULT
|
||||
},
|
||||
id_counter,
|
||||
);
|
||||
self.html(&anchor_id)
|
||||
}
|
||||
|
||||
fn html(&self, anchor_id: &str) -> String {
|
||||
let mut additional_class = Cow::Borrowed(self.directive.classname());
|
||||
let title = &self.title;
|
||||
let content = &self.content;
|
||||
let indent = " ".repeat(self.indent);
|
||||
|
||||
let title_block = if self.collapsible { "summary" } else { "div" };
|
||||
|
||||
let title_html = if !title.is_empty() {
|
||||
Cow::Owned(format!(
|
||||
r##"{indent}<{title_block} class="admonition-title">
|
||||
{indent}
|
||||
{indent}{title}
|
||||
{indent}
|
||||
{indent}<a class="admonition-anchor-link" href="#{ANCHOR_ID_PREFIX}-{anchor_id}"></a>
|
||||
{indent}</{title_block}>
|
||||
"##
|
||||
))
|
||||
} else {
|
||||
Cow::Borrowed("")
|
||||
};
|
||||
|
||||
if !self.additional_classnames.is_empty() {
|
||||
let mut buffer = additional_class.into_owned();
|
||||
for additional_classname in &self.additional_classnames {
|
||||
buffer.push(' ');
|
||||
buffer.push_str(additional_classname);
|
||||
}
|
||||
|
||||
additional_class = Cow::Owned(buffer);
|
||||
}
|
||||
|
||||
let admonition_block = if self.collapsible { "details" } else { "div" };
|
||||
// Notes on the HTML template:
|
||||
// - the additional whitespace around the content are deliberate
|
||||
// In line with the commonmark spec, this allows the inner content to be
|
||||
// rendered as markdown paragraphs.
|
||||
format!(
|
||||
r#"
|
||||
{indent}<{admonition_block} id="{ANCHOR_ID_PREFIX}-{anchor_id}" class="admonition {additional_class}">
|
||||
{title_html}{indent}<div>
|
||||
{indent}
|
||||
{indent}{content}
|
||||
{indent}
|
||||
{indent}</div>
|
||||
{indent}</{admonition_block}>"#,
|
||||
)
|
||||
}
|
||||
|
||||
/// Strips all admonish syntax, leaving the plain content of the block.
|
||||
pub(crate) fn strip(&self) -> String {
|
||||
// Add in newlines to preserve line numbering for test output
|
||||
// These replace the code fences we stripped out
|
||||
format!("\n{}\n", self.content)
|
||||
}
|
||||
}
|
||||
|
||||
const ANCHOR_ID_PREFIX: &str = "admonition";
|
||||
const ANCHOR_ID_DEFAULT: &str = "default";
|
||||
116
src/resolve.rs
Normal file
116
src/resolve.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use crate::config::InstanceConfig;
|
||||
use crate::types::{AdmonitionDefaults, Directive};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// All information required to render an admonition.
|
||||
///
|
||||
/// i.e. all configured options have been resolved at this point.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct AdmonitionMeta {
|
||||
pub directive: Directive,
|
||||
pub title: String,
|
||||
pub additional_classnames: Vec<String>,
|
||||
pub collapsible: bool,
|
||||
}
|
||||
|
||||
impl AdmonitionMeta {
|
||||
pub fn from_info_string(
|
||||
info_string: &str,
|
||||
defaults: &AdmonitionDefaults,
|
||||
) -> Option<Result<Self, String>> {
|
||||
InstanceConfig::from_info_string(info_string)
|
||||
.map(|raw| raw.map(|raw| Self::resolve(raw, defaults)))
|
||||
}
|
||||
|
||||
/// Combine the per-admonition configuration with global defaults (and
|
||||
/// other logic) to resolve the values needed for rendering.
|
||||
fn resolve(raw: InstanceConfig, defaults: &AdmonitionDefaults) -> Self {
|
||||
let InstanceConfig {
|
||||
directive: raw_directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
} = raw;
|
||||
|
||||
// Use values from block, else load default value
|
||||
let title = title.or_else(|| defaults.title.clone());
|
||||
let collapsible = collapsible.unwrap_or(defaults.collapsible);
|
||||
|
||||
// Load the directive (and title, if one still not given)
|
||||
let (directive, title) = match (Directive::from_str(&raw_directive), title) {
|
||||
(Ok(directive), None) => (directive, ucfirst(&raw_directive)),
|
||||
(Err(_), None) => (Directive::Note, "Note".to_owned()),
|
||||
(Ok(directive), Some(title)) => (directive, title),
|
||||
(Err(_), Some(title)) => (Directive::Note, title),
|
||||
};
|
||||
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the first letter of `input` upppercase.
|
||||
///
|
||||
/// source: https://stackoverflow.com/a/38406885
|
||||
fn ucfirst(input: &str) -> String {
|
||||
let mut chars = input.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: " ".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Default::default()
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: Directive::Note,
|
||||
title: "Note".to_owned(),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw_with_defaults() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: " ".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Important!!!".to_owned()),
|
||||
collapsible: true,
|
||||
},
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: Directive::Note,
|
||||
title: "Important!!!".to_owned(),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
17
src/types.rs
17
src/types.rs
@@ -1,5 +1,16 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Book wide defaults that may be provided by the user.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
|
||||
pub(crate) struct AdmonitionDefaults {
|
||||
#[serde(default)]
|
||||
pub(crate) title: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) collapsible: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum Directive {
|
||||
Note,
|
||||
@@ -37,3 +48,9 @@ impl FromStr for Directive {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum RenderTextMode {
|
||||
Strip,
|
||||
Html,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user