Compare commits

...

28 Commits

Author SHA1 Message Date
Tom Milligan
6deaf1ea2b docs: fix mdbook-toc build failure 2023-09-09 09:12:07 +01:00
Tom Milligan
041e5a566f Merge pull request #121 from tommilligan/update-deps
chore: prep v1.11.0 release
2023-09-09 09:06:44 +01:00
Tom Milligan
99b5a235cf chore: prep v1.11.0 release 2023-09-09 09:05:49 +01:00
Tom Milligan
39edc4d92a chore: bump lockfile 2023-09-09 08:58:32 +01:00
Tom Milligan
7773213093 Merge pull request #118 from eitsupi/eitsupi-patch-1
ci(deploy): Use Ubuntu 20.04 for building linux gnu target binaries
2023-09-09 08:49:24 +01:00
eitsupi
e888fcd021 Use Ubuntu 20.04 for building linux gnu target binaries 2023-09-09 08:48:24 +01:00
Tom Milligan
95dc7582ad Merge pull request #119 from eitsupi/add-aarch64-musl
ci(deploy): deploy aarch64-unknown-linux-musl
2023-09-09 08:43:59 +01:00
eitsupi
b658eb6049 strip via Cargo 2023-09-09 08:31:25 +01:00
eitsupi
623291625a deploy aarch64-unknown-linux-musl 2023-09-09 08:31:25 +01:00
Tom Milligan
4dad5a86c8 chore: fix clippy 2023-09-09 08:28:21 +01:00
Tom Milligan
7e774f4655 chore: prep v1.10.2 release (#116) 2023-08-07 18:58:32 +01:00
Tom Milligan
823cefbcbc fix: unlink mdbook internal toml dependency from mdbook-admonish (#115)
* chore: bump dependencies

* fix: unlink mdbook internal toml dependency from mdbook-admonish
2023-08-07 18:55:02 +01:00
Tom Milligan
a6a2941821 chore: prep v1.10.1 release (#113) 2023-07-28 18:49:46 +01:00
Uriel
faf99a1b76 only modify summary with the admonition-title class (#112)
* only modify `summary` with the `admonition-title` class

* regenerate assets, update snapshot test, fix themes

---------

Co-authored-by: Tom Milligan <tom.milligan@uipath.com>
2023-07-28 18:44:05 +01:00
Jonas
afdc2b03d0 Improvement of links (#111) 2023-07-26 12:19:01 +01:00
Tom Milligan
e55df3e60b Merge pull request #110 from tommilligan/prep-1.10.0
chore: prep v1.10.0 release
2023-07-23 19:51:53 +01:00
Tom Milligan
f3d49b93de chore: prep v1.10.0 release 2023-07-23 19:47:37 +01:00
Tom Milligan
92caf95b34 chore: add notes for future v2 release 2023-07-23 19:38:24 +01:00
Tom Milligan
0742c6c1e8 Merge pull request #109 from tommilligan/run-doctests
feat: add support for test renderer, running doctests
2023-07-23 19:33:20 +01:00
Tom Milligan
60706be3e0 ci: bump MSRV to 1.66.0 2023-07-23 19:21:18 +01:00
Tom Milligan
f5a6b9ef0f ci: build docs on each merge 2023-07-23 19:13:10 +01:00
Tom Milligan
9361b7e7fa book: add configuration docs 2023-07-23 19:10:24 +01:00
Tom Milligan
24bef47b15 internal: restructure book.toml config parsing 2023-07-23 19:10:24 +01:00
Tom Milligan
0eb5fd35c3 internal: restructure configuration parsing 2023-07-23 17:53:19 +01:00
Tom Milligan
de539cd0fd internal: split up lib.rs 2023-07-22 11:32:40 +01:00
Tom Milligan
4842daea1c feat: add support for test renderer, running doctests 2023-07-22 10:44:11 +01:00
Tom Milligan
76212fccfb internal: refactor how whitespace is added 2023-07-22 10:43:47 +01:00
Tom Milligan
681c991a9a Merge pull request #98 from tommilligan/ci-publish-auto
ci: fix automatic crates.io publication
2023-05-05 09:52:54 +01:00
29 changed files with 2088 additions and 1616 deletions

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -2,6 +2,43 @@
## Unreleased
## 1.11.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

986
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.9.0"
version = "1.11.0"
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.10", 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.7.3"
toml_edit = { version = "0.19.8", 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"]

View File

@@ -123,19 +123,6 @@ Alternatively, pin to a specific version for a reproducible installation:
cargo install mdbook-admonish --vers "1.5.0" --locked
```
### Bail on error
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]
on_failure = "bail"
```
This may be useful for non-interative workflows.
### Process included files
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):
@@ -187,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:

View File

@@ -9,7 +9,11 @@ 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]

View 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

View File

@@ -1,9 +1,13 @@
# 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-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
```

View File

@@ -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
View 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`

View File

@@ -143,7 +143,7 @@ a.admonition-anchor-link {
}
// Admonition title
:is(.admonition-title, summary) {
:is(.admonition-title, summary.admonition-title) {
position: relative;
min-height: 4rem;
margin-block: 0;
@@ -233,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

View File

@@ -9,9 +9,12 @@ title = "mdbook-admonish-integration"
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "2.0.1" # 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]

View File

@@ -9,9 +9,12 @@ title = "mdbook-admonish-integration"
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "2.0.1" # 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]

View File

@@ -30,7 +30,7 @@
</div>
<div>
<p>Failed with:</p>
<pre><code>TOML parsing error: TOML parse error at line 1, column 8
<pre><code class="language-log">TOML parsing error: TOML parse error at line 1, column 8
|
1 | title=&quot;
| ^
@@ -38,7 +38,7 @@ invalid basic string
</code></pre>
<p>Original markdown input:</p>
<pre><code>```admonish title=&quot;
<pre><code class="language-markdown">```admonish title=&quot;
No title, only body
```
</code></pre>
@@ -72,4 +72,22 @@ No title, only body
</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>

View File

@@ -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

View File

@@ -29,3 +29,15 @@ Hidden on load
Nested code block
```
````
````admonish
```rust
let x = 10;
x = 20;
```
```rust
let x = 10;
let x = 20;
```
````

View File

@@ -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

View File

@@ -1 +1 @@
2.0.1
2.0.2

View File

@@ -75,7 +75,7 @@ a.admonition-anchor-link::before {
content: "§";
}
:is(.admonition-title, summary) {
:is(.admonition-title, summary.admonition-title) {
position: relative;
min-height: 4rem;
margin-block: 0;
@@ -86,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;
@@ -107,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;
}
@@ -136,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);
@@ -153,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);
@@ -170,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);
@@ -187,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);
@@ -204,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);
@@ -221,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);
@@ -238,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);
@@ -255,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);
@@ -272,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);
@@ -289,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);
@@ -306,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);
@@ -323,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);

60
src/book_config.rs Normal file
View 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
}
}

1009
src/lib.rs

File diff suppressed because it is too large Load Diff

735
src/markdown.rs Normal file
View File

@@ -0,0 +1,735 @@
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];
let admonition = match parse_admonition(
info_string.as_ref(),
admonition_defaults,
span_content,
on_failure,
) {
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));
}
}

View File

@@ -1,3 +1,78 @@
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,
) -> 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,
// FIXME return error messages to break build if configured
// Err(message) => return Some(Err(content)),
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}
"#
)),
})
}
OnFailure::Bail => Err(anyhow!("Error processing admonition, bailing:\n{content}")),
});
}
};
Some(Ok(Admonition::new(info, extracted.body)))
}
/// 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.
///
@@ -25,7 +100,7 @@ fn extract_admonish_body_start_index(content: &str) -> usize {
}
fn extract_admonish_body_end_index(content: &str) -> (usize, Fence) {
let fence_character = content.chars().rev().next().unwrap_or('`');
let fence_character = content.chars().next_back().unwrap_or('`');
let number_fence_characters = content
.chars()
.rev()
@@ -38,21 +113,21 @@ fn extract_admonish_body_end_index(content: &str) -> (usize, Fence) {
}
#[derive(Debug, PartialEq)]
pub(crate) struct Fence {
pub(crate) character: char,
pub(crate) length: usize,
struct Fence {
character: char,
length: usize,
}
impl Fence {
pub fn new(character: char, length: usize) -> Self {
fn new(character: char, length: usize) -> Self {
Self { character, length }
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct Extracted<'a> {
pub(crate) body: &'a str,
pub(crate) fence: Fence,
struct Extracted<'a> {
body: &'a str,
fence: Fence,
}
/// Given the whole text content of the code fence, extract the body.
@@ -61,7 +136,7 @@ pub(crate) struct Extracted<'a> {
/// but it's not really clear a good way of doing that.
///
/// ref: https://spec.commonmark.org/0.30/#fenced-code-blocks
pub(crate) fn extract_admonish_body(content: &str) -> Extracted<'_> {
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);

259
src/preprocessor.rs Normal file
View 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)
}
}

122
src/render.rs Normal file
View File

@@ -0,0 +1,122 @@
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,
}
impl<'a> Admonition<'a> {
pub(crate) fn new(info: AdmonitionMeta, content: &'a str) -> Self {
let AdmonitionMeta {
directive,
title,
additional_classnames,
collapsible,
} = info;
Self {
directive,
title,
content: Cow::Borrowed(content),
additional_classnames,
collapsible,
}
}
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 title_block = if self.collapsible { "summary" } else { "div" };
let title_html = if !title.is_empty() {
Cow::Owned(format!(
r##"<{title_block} class="admonition-title">
{title}
<a class="admonition-anchor-link" href="#{ANCHOR_ID_PREFIX}-{anchor_id}"></a>
</{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#"
<{admonition_block} id="{ANCHOR_ID_PREFIX}-{anchor_id}" class="admonition {additional_class}">
{title_html}<div>
{content}
</div>
</{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

@@ -34,7 +34,7 @@ impl AdmonitionMeta {
// Use values from block, else load default value
let title = title.or_else(|| defaults.title.clone());
let collapsible = collapsible.or(defaults.collapsible).unwrap_or_default();
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) {
@@ -89,4 +89,28 @@ mod test {
}
);
}
#[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

@@ -2,13 +2,13 @@ use serde::{Deserialize, Serialize};
use std::str::FromStr;
/// Book wide defaults that may be provided by the user.
#[derive(Deserialize, Serialize, Debug, Default)]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub(crate) struct AdmonitionDefaults {
#[serde(default)]
pub(crate) title: Option<String>,
#[serde(default)]
pub(crate) collapsible: Option<bool>,
pub(crate) collapsible: bool,
}
#[derive(Debug, PartialEq)]
@@ -48,3 +48,9 @@ impl FromStr for Directive {
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RenderTextMode {
Strip,
Html,
}