Compare commits

..

1 Commits

Author SHA1 Message Date
Tom Milligan
f96fe54e7d bug: fix relative paths 2022-11-26 22:38:02 +00:00
38 changed files with 1668 additions and 2957 deletions

View File

@@ -70,7 +70,7 @@ jobs:
rust:
- stable
- beta
- 1.66.0
- 1.60.0
experimental:
- false
# Run a canary test on nightly that's allowed to fail

View File

@@ -16,16 +16,14 @@ 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: 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
os: ubuntu-latest
name: x86_64-unknown-linux-gnu.tar.gz
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
@@ -58,19 +56,17 @@ jobs:
profile: minimal
target: ${{ matrix.target }}
- name: Setup | cross
if: endsWith(matrix.target, '-unknown-linux-musl')
uses: taiki-e/install-action@v2
with:
tool: cross
- name: Setup | musl tools
if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt install -y musl-tools
- name: Build | Build
if: ${{ !endsWith(matrix.target, '-unknown-linux-musl') }}
if: matrix.target != 'x86_64-unknown-linux-musl'
run: cargo build --release --target ${{ matrix.target }}
- name: Build | Build (musl)
if: endsWith(matrix.target, '-unknown-linux-musl')
run: cross build --release --target ${{ matrix.target }}
if: matrix.target == 'x86_64-unknown-linux-musl'
run: cargo build --release --target ${{ matrix.target }}
- name: Post Setup | Extract tag name
shell: bash
@@ -82,6 +78,7 @@ 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]
@@ -89,6 +86,7 @@ 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
@@ -111,14 +109,9 @@ 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: |
cat CHANGELOG.md | sed -n '/^## ${{ steps.extract_version.outputs.version }}$/,/^## /p' | sed '$d' > RELEASE.md
git log -1 --pretty='%s' > RELEASE.md
- name: Build | Publish
uses: softprops/action-gh-release@v1
with:
@@ -126,30 +119,3 @@ 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

View File

@@ -1,9 +1,8 @@
name: docs
on:
push:
branches:
- "main"
release:
types: [published]
workflow_dispatch:
permissions:
@@ -31,8 +30,6 @@ 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 Normal file
View File

@@ -0,0 +1,31 @@
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

View File

@@ -2,71 +2,9 @@
## 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
@@ -83,7 +21,7 @@ It unintentionally increased the MSRV from 1.66.0
### 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 collapsible admonition bodies ([#26](https://github.com/tommilligan/mdbook-admonish/pull/26), thanks [@gggto](https://github.com/gggto) for the suggestion and implementation!)
- Support collapsiable 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
@@ -156,11 +94,7 @@ 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 (yanked)
**Note:** This release has been yanked.
It unintentionally introduced a serious parsing bug.
## 1.3.0
### Added

1394
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "mdbook-admonish"
version = "1.11.1"
version = "1.8.0"
edition = "2021"
authors = ["Tom Milligan <code@tommilligan.net>"]
@@ -17,38 +17,32 @@ 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.72"
anyhow = "1.0.65"
clap = { version = "4", default_features = false, features = ["std", "derive"], 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 }
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 }
[dev-dependencies]
pretty_assertions = "1.4.0"
pretty_assertions = "1.3.0"
[features]
default = ["cli", "cli-install"]
# Enable the command line binary
cli = ["clap", "env_logger"]
cli = ["clap", "env_logger", "log"]
# Enable installation of files and configuration
cli-install = ["toml_edit"]

View File

@@ -123,16 +123,18 @@ Alternatively, pin to a specific version for a reproducible installation:
cargo install mdbook-admonish --vers "1.5.0" --locked
```
### Process included files
### Bail on error
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):
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:
```toml
[preprocessor.admonish]
after = ["links"]
on_failure = "bail"
```
This will expand `include` directives, before expanding `admonish` blocks.
This may be useful for non-interative workflows.
### Semantic Versioning
@@ -174,6 +176,8 @@ 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:

View File

@@ -9,13 +9,9 @@ title = "The mdbook-admonish book"
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`
[preprocessor.toc]
command = "mdbook-toc"
renderer = ["html"]
assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
[output]
[output.html]
additional-css = ["./mdbook-admonish.css"]
additional-css = ["././mdbook-admonish.css"]

View File

@@ -1,9 +0,0 @@
#!/bin/bash
set -exuo pipefail
cd "$(dirname "$0")"/../..
if ! mdbook-toc --version; then
cargo install mdbook-toc --version 0.14.1 --force
fi

View File

@@ -1,13 +1,9 @@
# mdbook-admonish
<!-- toc -->
## Intoduction
[![Latest version](https://img.shields.io/crates/v/mdbook-admonish.svg)](https://crates.io/crates/mdbook-admonish)
[![docs.rs](https://img.shields.io/docsrs/mdbook-admonish)](https://docs.rs/mdbook-admonish)
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.
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.
It turns this:
@@ -25,8 +21,6 @@ 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>`:
````
@@ -39,7 +33,7 @@ My example is the best!
My example is the best!
```
See the [list of directives](./reference.md#directives) for a full list of supported admonitions. You'll find:
See the [mkdocs-material docs](https://squidfunk.github.io/mkdocs-material/reference/admonitions/#supported-types) for a list of supported admonitions. You'll find:
- `info`
- `warning`
@@ -60,28 +54,8 @@ 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.
@@ -176,3 +150,17 @@ 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
```

View File

@@ -1,93 +1,5 @@
# 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.

View File

@@ -1,6 +0,0 @@
# 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`

View File

@@ -143,9 +143,8 @@ a.admonition-anchor-link {
}
// Admonition title
:is(.admonition-title, summary.admonition-title) {
:is(.admonition-title, summary) {
position: relative;
min-height: 4rem;
margin-block: 0;
margin-inline: -1.6rem -1.2rem;
padding-block: 0.8rem;
@@ -233,7 +232,7 @@ summary.admonition-title {
}
// Admonition flavour title
:is(#{$flavours}) > :is(.admonition-title, summary.admonition-title) {
:is(#{$flavours}) > :is(.admonition-title, summary) {
background-color: color.adjust($tint, $alpha: -0.9);
// Admonition icon

View File

@@ -9,11 +9,7 @@ title = "mdbook-admonish-integration"
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`
after = ["links"]
[preprocessor.admonish.renderer.test]
render_mode = "strip"
assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
[output]

View File

@@ -9,11 +9,7 @@ title = "mdbook-admonish-integration"
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`
after = ["links"]
[preprocessor.admonish.renderer.test]
render_mode = "strip"
assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
[output]

View File

@@ -29,16 +29,9 @@
<p><a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a></p>
</div>
<div>
<p>Failed with:</p>
<pre><code class="language-log">TOML parsing error: TOML parse error at line 1, column 8
|
1 | title=&quot;
| ^
invalid basic string
</code></pre>
<p>Failed with: TOML parsing error: unterminated string at line 1 column 7</p>
<p>Original markdown input:</p>
<pre><code class="language-markdown">```admonish title=&quot;
<pre><code>```admonish title=&quot;
No title, only body
```
</code></pre>
@@ -53,66 +46,4 @@ 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>

View File

@@ -51,21 +51,7 @@ 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/scripts/update-snapshot"
eprintln "./integration/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

View File

@@ -21,43 +21,3 @@ 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
```

View File

@@ -1,3 +0,0 @@
```admonish warning
This is a commonly shared warning!
```

View File

@@ -1,14 +0,0 @@
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]"

View File

@@ -11,6 +11,18 @@ 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`
# - RUSTSEC-2021-0145 known unmaintained atty transitive dep
eprintln "Auditing dependencies"
cargo audit --deny warnings \
--ignore RUSTSEC-2020-0071 \
--ignore RUSTSEC-2020-0016 \
--ignore RUSTSEC-2021-0145 \
--ignore RUSTSEC-2020-0159
eprintln "Linting sources"
cargo clippy --all-targets -- -D warnings

View File

@@ -10,4 +10,8 @@ cd "$(dirname "$0")"/..
rustup component add rustfmt clippy
if ! cargo audit --version; then
cargo install cargo-audit --force
fi
./scripts/install-mdbook

View File

@@ -5,5 +5,5 @@ set -exuo pipefail
cd "$(dirname "$0")"/..
if ! mdbook --version; then
cargo install mdbook --version 0.4.32 --force
cargo install mdbook --force
fi

View File

@@ -1 +1 @@
2.0.2
2.0.0

View File

@@ -75,9 +75,8 @@ a.admonition-anchor-link::before {
content: "§";
}
:is(.admonition-title, summary.admonition-title) {
:is(.admonition-title, summary) {
position: relative;
min-height: 4rem;
margin-block: 0;
margin-inline: -1.6rem -1.2rem;
padding-block: 0.8rem;
@@ -86,13 +85,13 @@ a.admonition-anchor-link::before {
background-color: rgba(68, 138, 255, 0.1);
display: flex;
}
:is(.admonition-title, summary.admonition-title) p {
:is(.admonition-title, summary) p {
margin: 0;
}
html :is(.admonition-title, summary.admonition-title):last-child {
html :is(.admonition-title, summary):last-child {
margin-bottom: 0;
}
:is(.admonition-title, summary.admonition-title)::before {
:is(.admonition-title, summary)::before {
position: absolute;
top: 0.625em;
inset-inline-start: 1.6rem;
@@ -107,7 +106,7 @@ html :is(.admonition-title, summary.admonition-title):last-child {
-webkit-mask-size: contain;
content: "";
}
:is(.admonition-title, summary.admonition-title):hover a.admonition-anchor-link {
:is(.admonition-title, summary):hover a.admonition-anchor-link {
display: initial;
}
@@ -136,10 +135,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #448aff;
}
:is(.note) > :is(.admonition-title, summary.admonition-title) {
:is(.note) > :is(.admonition-title, summary) {
background-color: rgba(68, 138, 255, 0.1);
}
:is(.note) > :is(.admonition-title, summary.admonition-title)::before {
:is(.note) > :is(.admonition-title, summary)::before {
background-color: #448aff;
mask-image: var(--md-admonition-icon--note);
-webkit-mask-image: var(--md-admonition-icon--note);
@@ -153,10 +152,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #00b0ff;
}
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title) {
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary) {
background-color: rgba(0, 176, 255, 0.1);
}
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title)::before {
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary)::before {
background-color: #00b0ff;
mask-image: var(--md-admonition-icon--abstract);
-webkit-mask-image: var(--md-admonition-icon--abstract);
@@ -170,10 +169,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #00b8d4;
}
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title) {
:is(.info, .todo) > :is(.admonition-title, summary) {
background-color: rgba(0, 184, 212, 0.1);
}
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title)::before {
:is(.info, .todo) > :is(.admonition-title, summary)::before {
background-color: #00b8d4;
mask-image: var(--md-admonition-icon--info);
-webkit-mask-image: var(--md-admonition-icon--info);
@@ -187,10 +186,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #00bfa5;
}
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title) {
:is(.tip, .hint, .important) > :is(.admonition-title, summary) {
background-color: rgba(0, 191, 165, 0.1);
}
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title)::before {
:is(.tip, .hint, .important) > :is(.admonition-title, summary)::before {
background-color: #00bfa5;
mask-image: var(--md-admonition-icon--tip);
-webkit-mask-image: var(--md-admonition-icon--tip);
@@ -204,10 +203,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #00c853;
}
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title) {
:is(.success, .check, .done) > :is(.admonition-title, summary) {
background-color: rgba(0, 200, 83, 0.1);
}
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title)::before {
:is(.success, .check, .done) > :is(.admonition-title, summary)::before {
background-color: #00c853;
mask-image: var(--md-admonition-icon--success);
-webkit-mask-image: var(--md-admonition-icon--success);
@@ -221,10 +220,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #64dd17;
}
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title) {
:is(.question, .help, .faq) > :is(.admonition-title, summary) {
background-color: rgba(100, 221, 23, 0.1);
}
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title)::before {
:is(.question, .help, .faq) > :is(.admonition-title, summary)::before {
background-color: #64dd17;
mask-image: var(--md-admonition-icon--question);
-webkit-mask-image: var(--md-admonition-icon--question);
@@ -238,10 +237,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #ff9100;
}
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title) {
:is(.warning, .caution, .attention) > :is(.admonition-title, summary) {
background-color: rgba(255, 145, 0, 0.1);
}
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title)::before {
:is(.warning, .caution, .attention) > :is(.admonition-title, summary)::before {
background-color: #ff9100;
mask-image: var(--md-admonition-icon--warning);
-webkit-mask-image: var(--md-admonition-icon--warning);
@@ -255,10 +254,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #ff5252;
}
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title) {
:is(.failure, .fail, .missing) > :is(.admonition-title, summary) {
background-color: rgba(255, 82, 82, 0.1);
}
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title)::before {
:is(.failure, .fail, .missing) > :is(.admonition-title, summary)::before {
background-color: #ff5252;
mask-image: var(--md-admonition-icon--failure);
-webkit-mask-image: var(--md-admonition-icon--failure);
@@ -272,10 +271,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #ff1744;
}
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title) {
:is(.danger, .error) > :is(.admonition-title, summary) {
background-color: rgba(255, 23, 68, 0.1);
}
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title)::before {
:is(.danger, .error) > :is(.admonition-title, summary)::before {
background-color: #ff1744;
mask-image: var(--md-admonition-icon--danger);
-webkit-mask-image: var(--md-admonition-icon--danger);
@@ -289,10 +288,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #f50057;
}
:is(.bug) > :is(.admonition-title, summary.admonition-title) {
:is(.bug) > :is(.admonition-title, summary) {
background-color: rgba(245, 0, 87, 0.1);
}
:is(.bug) > :is(.admonition-title, summary.admonition-title)::before {
:is(.bug) > :is(.admonition-title, summary)::before {
background-color: #f50057;
mask-image: var(--md-admonition-icon--bug);
-webkit-mask-image: var(--md-admonition-icon--bug);
@@ -306,10 +305,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #7c4dff;
}
:is(.example) > :is(.admonition-title, summary.admonition-title) {
:is(.example) > :is(.admonition-title, summary) {
background-color: rgba(124, 77, 255, 0.1);
}
:is(.example) > :is(.admonition-title, summary.admonition-title)::before {
:is(.example) > :is(.admonition-title, summary)::before {
background-color: #7c4dff;
mask-image: var(--md-admonition-icon--example);
-webkit-mask-image: var(--md-admonition-icon--example);
@@ -323,10 +322,10 @@ details[open].admonition > summary.admonition-title::after {
border-color: #9e9e9e;
}
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title) {
:is(.quote, .cite) > :is(.admonition-title, summary) {
background-color: rgba(158, 158, 158, 0.1);
}
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title)::before {
:is(.quote, .cite) > :is(.admonition-title, summary)::before {
background-color: #9e9e9e;
mask-image: var(--md-admonition-icon--quote);
-webkit-mask-image: var(--md-admonition-icon--quote);

View File

@@ -144,7 +144,7 @@ mod install {
let mut additional_css = additional_css(&mut doc);
for (name, content) in ADMONISH_CSS_FILES {
let filepath = proj_dir.join(css_dir.clone()).join(name);
let filepath = proj_dir.join(&css_dir).join(name);
// Normalize path to remove no-op components
// https://github.com/tommilligan/mdbook-admonish/issues/47
let filepath: PathBuf = filepath.components().collect();

View File

@@ -1,60 +0,0 @@
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
}
}

View File

@@ -1,16 +1,15 @@
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 InstanceConfig {
pub(crate) directive: String,
pub(crate) title: Option<String>,
pub(crate) additional_classnames: Vec<String>,
pub(crate) collapsible: Option<bool>,
pub(crate) struct AdmonitionInfoRaw {
directive: String,
title: Option<String>,
additional_classnames: Vec<String>,
collapsible: bool,
}
/// Extract the remaining info string, if this is an admonition block.
@@ -28,10 +27,10 @@ fn admonition_config_string(info_string: &str) -> Option<&str> {
}
}
impl InstanceConfig {
impl AdmonitionInfoRaw {
/// Returns:
/// - `None` if this is not an `admonish` block.
/// - `Some(InstanceConfig)` if this is an `admonish` block
/// - `Some(AdmonitionInfoRaw)` 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)?;
@@ -41,13 +40,65 @@ impl InstanceConfig {
Err(config) => config,
};
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)
})
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(),
}
}
@@ -59,30 +110,48 @@ mod test {
#[test]
fn test_from_info_string() {
// Not admonition blocks
assert_eq!(InstanceConfig::from_info_string(""), None);
assert_eq!(InstanceConfig::from_info_string("adm"), None);
assert_eq!(AdmonitionInfoRaw::from_info_string(""), None);
assert_eq!(AdmonitionInfoRaw::from_info_string("adm"), None);
// v1 syntax is supported back compatibly
assert_eq!(
InstanceConfig::from_info_string("admonish note.additional-classname")
AdmonitionInfoRaw::from_info_string("admonish note.additional-classname")
.unwrap()
.unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "note".to_owned(),
title: None,
additional_classnames: vec!["additional-classname".to_owned()],
collapsible: None,
collapsible: false,
}
);
// v2 syntax is supported
assert_eq!(
InstanceConfig::from_info_string(r#"admonish title="Custom Title" type="question""#)
AdmonitionInfoRaw::from_info_string(r#"admonish title="Custom Title" type="question""#)
.unwrap()
.unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "question".to_owned(),
title: Some("Custom Title".to_owned()),
additional_classnames: Vec::new(),
collapsible: None,
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,
}
);
}

View File

@@ -1,8 +1,8 @@
use super::InstanceConfig;
use super::AdmonitionInfoRaw;
use once_cell::sync::Lazy;
use regex::Regex;
pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig, String> {
pub(crate) fn from_config_string(config_string: &str) -> Result<AdmonitionInfoRaw, 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<InstanceConfig,
),
};
Ok(InstanceConfig {
Ok(AdmonitionInfoRaw {
directive: directive.to_owned(),
title,
additional_classnames,
collapsible: None,
collapsible: false,
})
}
@@ -66,47 +66,47 @@ mod test {
fn test_from_config_string() {
assert_eq!(
from_config_string("").unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "".to_owned(),
title: None,
additional_classnames: Vec::new(),
collapsible: None,
collapsible: false,
}
);
assert_eq!(
from_config_string(" ").unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "".to_owned(),
title: None,
additional_classnames: Vec::new(),
collapsible: None,
collapsible: false,
}
);
assert_eq!(
from_config_string("unknown").unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "unknown".to_owned(),
title: None,
additional_classnames: Vec::new(),
collapsible: None,
collapsible: false,
}
);
assert_eq!(
from_config_string("note").unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "note".to_owned(),
title: None,
additional_classnames: Vec::new(),
collapsible: None,
collapsible: false,
}
);
assert_eq!(
from_config_string("note.additional-classname").unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "note".to_owned(),
title: None,
additional_classnames: vec!["additional-classname".to_owned()],
collapsible: None,
collapsible: false,
}
);
}

View File

@@ -1,10 +1,10 @@
use super::InstanceConfig;
use super::AdmonitionInfoRaw;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::Deserialize;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
struct UserInput {
struct AdmonitionInfoConfig {
#[serde(default)]
r#type: Option<String>,
#[serde(default)]
@@ -12,7 +12,7 @@ struct UserInput {
#[serde(default)]
class: Option<String>,
#[serde(default)]
collapsible: Option<bool>,
collapsible: 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<InstanceConfig, String> {
pub(crate) fn from_config_string(config_string: &str) -> Result<AdmonitionInfoRaw, String> {
let config_toml = bare_key_value_pairs_to_toml(config_string);
let config_toml = config_toml.trim();
let config: UserInput = match toml::from_str(config_toml) {
let config: AdmonitionInfoConfig = 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<InstanceConfig,
return original_error;
}
let mut config: UserInput = match toml::from_str(config_toml) {
let mut config: AdmonitionInfoConfig = 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<InstanceConfig,
.collect()
})
.unwrap_or_default();
Ok(InstanceConfig {
Ok(AdmonitionInfoRaw {
directive: config.r#type.unwrap_or_default(),
title: config.title,
additional_classnames,
@@ -102,62 +102,60 @@ mod test {
fn test_from_config_string_v2() {
assert_eq!(
from_config_string("").unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "".to_owned(),
title: None,
additional_classnames: Vec::new(),
collapsible: None,
collapsible: false,
}
);
assert_eq!(
from_config_string(" ").unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "".to_owned(),
title: None,
additional_classnames: Vec::new(),
collapsible: None,
collapsible: false,
}
);
assert_eq!(
from_config_string(
r#"type="note" class="additional classname" title="Никита" collapsible=true"#
)
.unwrap(),
InstanceConfig {
from_config_string(r#"type="note" class="additional classname" title="Никита""#)
.unwrap(),
AdmonitionInfoRaw {
directive: "note".to_owned(),
title: Some("Никита".to_owned()),
additional_classnames: vec!["additional".to_owned(), "classname".to_owned()],
collapsible: Some(true),
collapsible: false,
}
);
// Specifying unknown keys is okay, as long as they're valid
assert_eq!(
from_config_string(r#"unkonwn="but valid toml""#).unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "".to_owned(),
title: None,
additional_classnames: Vec::new(),
collapsible: None,
collapsible: false,
}
);
// Just directive is fine
assert_eq!(
from_config_string(r#"info"#).unwrap(),
InstanceConfig {
AdmonitionInfoRaw {
directive: "info".to_owned(),
title: None,
additional_classnames: Vec::new(),
collapsible: None,
collapsible: false,
}
);
// Directive plus toml config
assert_eq!(
from_config_string(r#"info title="Information" collapsible=false"#).unwrap(),
InstanceConfig {
from_config_string(r#"info title="Information""#).unwrap(),
AdmonitionInfoRaw {
directive: "info".to_owned(),
title: Some("Information".to_owned()),
additional_classnames: Vec::new(),
collapsible: Some(false),
collapsible: false,
}
);
// Directive after toml config is an error
@@ -168,12 +166,7 @@ mod test {
fn test_from_config_string_invalid_toml_value() {
assert_eq!(
from_config_string(r#"note titlel=""#).unwrap_err(),
r#"TOML parsing error: TOML parse error at line 1, column 6
|
1 | note
| ^
expected `.`, `=`
"#
"TOML parsing error: expected an equals, found a newline at line 1 column 6".to_owned()
);
}
}

View File

@@ -1,10 +1,781 @@
mod book_config;
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 config;
mod markdown;
mod parse;
mod preprocessor;
mod render;
mod resolve;
mod types;
pub use crate::preprocessor::Admonish;
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));
}
}

View File

@@ -1,806 +0,0 @@
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));
}
}

View File

@@ -1,240 +0,0 @@
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);
}
}
}

View File

@@ -1,259 +0,0 @@
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)
}
}

View File

@@ -1,125 +0,0 @@
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";

View File

@@ -1,116 +0,0 @@
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,
}
);
}
}

View File

@@ -1,16 +1,5 @@
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,
@@ -48,9 +37,3 @@ impl FromStr for Directive {
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RenderTextMode {
Strip,
Html,
}