mirror of
https://github.com/tommilligan/mdbook-admonish.git
synced 2025-12-28 11:24:09 -05:00
Compare commits
42 Commits
v1.3.3
...
better-err
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92b441a950 | ||
|
|
5530da074b | ||
|
|
0fe2ad52ed | ||
|
|
787744f1f1 | ||
|
|
603c83e2eb | ||
|
|
fbdffaa723 | ||
|
|
7c20d5b2d1 | ||
|
|
3507d6b5e0 | ||
|
|
f81d4d40dd | ||
|
|
ed019a92d9 | ||
|
|
9dd2ca128c | ||
|
|
51120acfd9 | ||
|
|
650123645b | ||
|
|
438c1dff5a | ||
|
|
78b7451e49 | ||
|
|
bb937dc2d2 | ||
|
|
db7101cb12 | ||
|
|
fd6c2d0bd0 | ||
|
|
72deb8421c | ||
|
|
7b5a13d6af | ||
|
|
28dfc5b6c3 | ||
|
|
beb640077f | ||
|
|
6b479255a6 | ||
|
|
7fe2fdc329 | ||
|
|
e61e6148b5 | ||
|
|
0e235cb9b5 | ||
|
|
c28f2b2fc9 | ||
|
|
2a78ccbc2a | ||
|
|
5b61637eb7 | ||
|
|
9786269b49 | ||
|
|
6517f50353 | ||
|
|
6dc759e358 | ||
|
|
99f17dd2c5 | ||
|
|
7c6f878d6c | ||
|
|
3fa5067be9 | ||
|
|
8630161fa3 | ||
|
|
0b50fd68ba | ||
|
|
d851076cbc | ||
|
|
af038017d2 | ||
|
|
9fc872b8b8 | ||
|
|
b667271080 | ||
|
|
0417d55ab8 |
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
40
.github/workflows/check.yml
vendored
40
.github/workflows/check.yml
vendored
@@ -1,17 +1,43 @@
|
||||
on: [push, pull_request]
|
||||
on: [pull_request]
|
||||
|
||||
name: check
|
||||
|
||||
jobs:
|
||||
# Fast test before we kick off all the other jobs
|
||||
fast-test:
|
||||
name: Fast test
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: fast-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Install more toolchain
|
||||
run: rustup component add rustfmt clippy
|
||||
- name: Run tests
|
||||
run: cargo clippy --all-targets -- -D warnings && cargo fmt -- --check && cargo test
|
||||
|
||||
# Test, and also do other things like doctests and examples
|
||||
detailed-test:
|
||||
needs: fast-test
|
||||
name: Test main target
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
@@ -34,7 +60,7 @@ jobs:
|
||||
|
||||
# Test on all supported platforms
|
||||
test:
|
||||
needs: detailed-test
|
||||
needs: fast-test
|
||||
name: Test all other targets
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -44,7 +70,7 @@ jobs:
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- 1.58.0
|
||||
- 1.60.0
|
||||
experimental:
|
||||
- false
|
||||
# Run a canary test on nightly that's allowed to fail
|
||||
@@ -61,9 +87,9 @@ jobs:
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
|
||||
121
.github/workflows/deploy.yml
vendored
Normal file
121
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
# Based on https://github.com/starship/starship/blob/master/.github/workflows/deploy.yml
|
||||
|
||||
name: Deploy
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
CRATE_NAME: mdbook-admonish
|
||||
|
||||
jobs:
|
||||
# Build sources for every OS
|
||||
github_build:
|
||||
name: Build release binaries
|
||||
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
|
||||
os: ubuntu-latest
|
||||
name: x86_64-unknown-linux-gnu.tar.gz
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
name: x86_64-unknown-linux-musl.tar.gz
|
||||
- target: x86_64-apple-darwin
|
||||
os: macOS-latest
|
||||
name: x86_64-apple-darwin.tar.gz
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
name: x86_64-pc-windows-msvc.zip
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Setup | Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Cache files between builds
|
||||
- name: Setup | Cache Cargo
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Setup | Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
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: Build | Build
|
||||
if: matrix.target != 'x86_64-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 }}
|
||||
|
||||
- name: Post Setup | Extract tag name
|
||||
shell: bash
|
||||
run: echo "##[set-output name=tag;]$(echo ${GITHUB_REF#refs/tags/})"
|
||||
id: extract_tag
|
||||
|
||||
- name: Post Setup | Prepare artifacts [Windows]
|
||||
if: matrix.os == 'windows-latest'
|
||||
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]
|
||||
if: matrix.os != 'windows-latest'
|
||||
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
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.CRATE_NAME }}-${{ steps.extract_tag.outputs.tag }}-${{ matrix.name }}
|
||||
path: target/stage/*
|
||||
|
||||
# Create GitHub release with Rust build targets and release notes
|
||||
github_release:
|
||||
name: Create GitHub Release
|
||||
needs: github_build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup | Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup | Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Setup | Release notes
|
||||
run: |
|
||||
git log -1 --pretty='%s' > RELEASE.md
|
||||
- name: Build | Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ${{ env.CRATE_NAME }}-*/${{ env.CRATE_NAME }}-*
|
||||
body_path: RELEASE.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
39
.github/workflows/docs.yml
vendored
Normal file
39
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: docs
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
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: Install mdbook
|
||||
run: ./scripts/install-mdbook
|
||||
- name: Build book
|
||||
run: ./scripts/build-book
|
||||
- name: Push docs
|
||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: book/book
|
||||
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@@ -9,8 +9,8 @@ jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
|
||||
69
CHANGELOG.md
69
CHANGELOG.md
@@ -1,5 +1,74 @@
|
||||
## Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.8.0
|
||||
|
||||
### Changed
|
||||
|
||||
- MSRV (minimum supported rust version) is now 1.60.0 for clap v4
|
||||
|
||||
## 1.7.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Required styles version is now `^2.0.0` (release `1.7.0`). Run `mdbook-admonish install` to update.
|
||||
|
||||
### Added
|
||||
|
||||
- Support key/value configuration ([#24](https://github.com/tommilligan/mdbook-admonish/pull/24), thanks [@gggto](https://github.com/gggto) and [@schungx](https://github.com/schungx) for design input)
|
||||
- Support collapsiable admonition bodies ([#26](https://github.com/tommilligan/mdbook-admonish/pull/26), thanks [@gggto](https://github.com/gggto) for the suggestion and implementation!)
|
||||
- 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
|
||||
- Option to fail the build instead
|
||||
|
||||
## 1.6.0
|
||||
|
||||
**Please note:** If updating from an older version, this release requires `mdboook-admonish install` to be rerun after installation.
|
||||
|
||||
This behaviour is [documented in the readme here](https://github.com/tommilligan/mdbook-admonish#semantic-versioning), and may appear in any future minor version release.
|
||||
|
||||
### Changed
|
||||
|
||||
- Required styles version is now `^1.0.0` (release `1.6.0`). Run `mdbook-admonish install` to update.
|
||||
|
||||
### Added
|
||||
|
||||
- Enforce updating installed styles when required for new features ([#19](https://github.com/tommilligan/mdbook-admonish/pull/19)
|
||||
- Each admonition has a unique id. Click the header bar to navigate to the anchor link ([#19](https://github.com/tommilligan/mdbook-admonish/pull/19), thanks [@schungx](https://github.com/schungx) for the suggestion)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Header bar overflow at some zoom levels on Firefox ([#21](https://github.com/tommilligan/mdbook-admonish/pull/21), thanks to [@sgoudham](https://github.com/sgoudham) for the report)
|
||||
|
||||
## 1.5.0
|
||||
|
||||
### Added
|
||||
|
||||
- Admonitions now have an autogenerated `id`, to support anchor links ([#16](https://github.com/tommilligan/mdbook-admonish/pull/16), thanks [@schungx](https://github.com/schungx) for the suggestion)
|
||||
|
||||
## 1.4.1
|
||||
|
||||
### Changed
|
||||
|
||||
- Bumped locked dependency versions (mdbook v0.4.18)
|
||||
|
||||
### Packaging
|
||||
|
||||
- Support building and releasing binary artefacts.
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Added
|
||||
|
||||
- Additional classnames can be specified using `directive.classname` syntax
|
||||
- Support removing the title bar entirely
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed superfluous empty `<p>` tags in output
|
||||
|
||||
## 1.3.3
|
||||
|
||||
### Fixed
|
||||
|
||||
1242
Cargo.lock
generated
1242
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "mdbook-admonish"
|
||||
version = "1.3.3"
|
||||
version = "1.8.0"
|
||||
edition = "2021"
|
||||
|
||||
authors = ["Tom Milligan <code@tommilligan.net>"]
|
||||
description = "A preprocessor for mdbook to add Material Design admonishments."
|
||||
repository = "https://github.com/tommilligan/mdbook-admonish"
|
||||
documentation = "https://docs.rs/mdbook-admonish"
|
||||
documentation = "https://tommilligan.github.io/mdbook-admonish/"
|
||||
|
||||
license = "MIT"
|
||||
keywords = ["mdbook", "markdown", "material", "design", "ui"]
|
||||
@@ -22,16 +22,22 @@ name = "mdbook_admonish"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "3.0.14", default_features = false, features = ["std", "cargo"], optional = true }
|
||||
env_logger = { version = "0.9.0", default_features = false, optional = true }
|
||||
log = { version = "0.4.14", optional = true }
|
||||
mdbook = "0.4.15"
|
||||
pulldown-cmark = "0.9.1"
|
||||
serde_json = "1.0.79"
|
||||
toml_edit = { version = "0.13.4", optional = true }
|
||||
anyhow = "1.0.65"
|
||||
clap = { version = "4", default_features = false, features = ["std", "derive"], optional = true }
|
||||
env_logger = { version = "0.9.1", default_features = false, optional = true }
|
||||
log = { version = "0.4.17", optional = true }
|
||||
mdbook = "0.4.21"
|
||||
once_cell = "1.15.0"
|
||||
pulldown-cmark = "0.9.2"
|
||||
regex = "1.6.0"
|
||||
semver = "1.0.14"
|
||||
serde = { version = "1.0.145", features = ["derive"] }
|
||||
serde_json = "1.0.85"
|
||||
toml = "0.5.9"
|
||||
toml_edit = { version = "0.15.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.1.0"
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
[features]
|
||||
default = ["cli", "cli-install"]
|
||||
|
||||
126
README.md
126
README.md
@@ -1,10 +1,9 @@
|
||||
# mdbook-admonish
|
||||
|
||||
[](https://crates.io/crates/mdbook-admonish)
|
||||
[](https://docs.rs/mdbook-admonish)
|
||||
[](https://tommilligan.github.io/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-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:
|
||||
|
||||
@@ -18,6 +17,14 @@ into this:
|
||||
|
||||

|
||||
|
||||
## Examples
|
||||
|
||||
Read the documentation [here](https://tommilligan.github.io/mdbook-admonish/), to see the actual examples in action. You can see the source in the [`./book`](./book) subdirectory.
|
||||
|
||||
Other projects using mdbook-admonish:
|
||||
|
||||
- [The Rhai Book](https://rhai.rs/book/)
|
||||
|
||||
## Usage
|
||||
|
||||
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>`:
|
||||
@@ -30,7 +37,7 @@ 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 [reference page](https://tommilligan.github.io/mdbook-admonish/reference.html) for a list of supported admonitions. You'll find:
|
||||
|
||||
- `info`
|
||||
- `warning`
|
||||
@@ -39,19 +46,6 @@ See the [mkdocs-material docs](https://squidfunk.github.io/mkdocs-material/refer
|
||||
|
||||
and quite a few more!
|
||||
|
||||
### Additional Options
|
||||
|
||||
A custom title can be provided, contained in a double quoted JSON string.
|
||||
Note that JSON escapes must be escaped again - for instance, write `\"` as `\\"`.
|
||||
|
||||
````
|
||||
```admonish warning "Data loss"
|
||||
The following steps can lead to irrecoverable data corruption.
|
||||
```
|
||||
````
|
||||
|
||||

|
||||
|
||||
You can also leave out the admonition type altogether, in which case it will default to `note`:
|
||||
|
||||
````
|
||||
@@ -62,30 +56,13 @@ A plain note.
|
||||
|
||||

|
||||
|
||||
Markdown and HTML can be used in the inner content, as you'd expect:
|
||||
### Additional Options
|
||||
|
||||
````
|
||||
```admonish tip "_Referencing_ and <i>dereferencing</i>"
|
||||
The opposite of *referencing* by using `&` is *dereferencing*, which is
|
||||
accomplished with the <span style="color: hotpink">dereference operator</span>, `*`.
|
||||
```
|
||||
````
|
||||
See the [`mdbook-admonish` book](https://tommilligan.github.io/mdbook-admonish/) for additional options, such as:
|
||||
|
||||

|
||||
|
||||
If you have code blocks you want to include in the content,
|
||||
use [tildes for the outer code fence](https://spec.commonmark.org/0.30/#fenced-code-blocks):
|
||||
|
||||
````
|
||||
~~~admonish bug
|
||||
This syntax won't work in Python 3:
|
||||
```python
|
||||
print "Hello, world!"
|
||||
```
|
||||
~~~
|
||||
````
|
||||
|
||||

|
||||
- Custom titles
|
||||
- Custom styling
|
||||
- Collapsible blocks
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -98,6 +75,8 @@ cargo install mdbook-admonish
|
||||
Then let `mdbook-admonish` add the required files and configuration:
|
||||
|
||||
```bash
|
||||
# Note: this may need to be rerun for new minor versions of mdbook-admonish
|
||||
# see the 'Semantic Versioning' section below for details.
|
||||
mdbook-admonish install path/to/your/book
|
||||
|
||||
# optionally, specify a directory where CSS files live, relative to the book root
|
||||
@@ -124,9 +103,51 @@ mdbook path/to/book
|
||||
|
||||
### Updates
|
||||
|
||||
**Please note**, when updating your version of `mdbook-admonish`, updated styles
|
||||
will not be applied unless you rerun `mdbook-admonish install` to update the additional
|
||||
CSS files in your book.
|
||||
**Please note**, when updating your version of `mdbook-admonish`, updated styles will not be applied unless you rerun `mdbook-admonish install` to update the additional CSS files in your book.
|
||||
|
||||
`mdbook` will fail the build if you require newer assets than you have installed:
|
||||
|
||||
```log
|
||||
2022-04-26 12:27:52 [INFO] (mdbook::book): Book building has started
|
||||
ERROR:
|
||||
Incompatible assets installed: required mdbook-admonish assets version '^2.0.0', but found '1.0.0'.
|
||||
Please run `mdbook-admonish install` to update installed assets.
|
||||
2022-04-26 12:27:52 [ERROR] (mdbook::utils): Error: The "admonish" preprocessor exited unsuccessfully with exit status: 1 status
|
||||
```
|
||||
|
||||
If you want to update across minor versions without breakage, you should always run `mdbook-admonish install`.
|
||||
|
||||
Alternatively, pin to a specific version for a reproducible installation:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
### Semantic Versioning
|
||||
|
||||
Guarantees provided are as follows:
|
||||
|
||||
- Major versions: Contain breaking changes to the user facing markdown API, or the public API of the crate itself.
|
||||
- Minor versions: Feature release. May contain changes to generated CSS/HTML requiring `mdbook-admonish install` to be rerun.
|
||||
- **Note:** updating acrosss minor versions without running `mdbook-admonish install` to reinstall assets may break your build.
|
||||
- This is due to limitations in the `mdbook` preprocessor architecture. Relevant issues that may alleviate this:
|
||||
- https://github.com/rust-lang/mdBook/issues/1222
|
||||
- https://github.com/rust-lang/mdBook/issues/1687
|
||||
- https://github.com/rust-lang/mdBook/issues/1689
|
||||
- Patch versions: Bug fixes only.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -136,6 +157,27 @@ Project design
|
||||
- `mdbook-admonish install` is responsible for delivering additional assets and configuration to a client book.
|
||||
- `mdbook-admonish` is responsible for preprocessing book data, adding HTML that references compiled classnames.
|
||||
|
||||
### Scripts to get started
|
||||
|
||||
- `./scripts/install` installs other toolchains required for development
|
||||
- `./scripts/check` runs a full CI check
|
||||
- `./scripts/rebuild-book` rebuilds the reference book under `./book`. This is useful for integration testing locally.
|
||||
|
||||
### Making breaking changes in CSS
|
||||
|
||||
To make a breaking change in CSS, you should:
|
||||
|
||||
- Update the assets version in `./src/bin/assets/VERSION`
|
||||
- Update the required assets version specifier in `./src/REQUIRED_ASSETS_VERSION`
|
||||
|
||||
You must make the next `mdbook-admonish` crate version at least a **minor** version bump.
|
||||
|
||||
### Releasing
|
||||
|
||||
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:
|
||||
|
||||
2
book/.gitignore
vendored
Normal file
2
book/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/book
|
||||
/mdbook-admonish.css
|
||||
17
book/book.toml
Normal file
17
book/book.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[book]
|
||||
authors = ["Tom Milligan"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "The mdbook-admonish book"
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["././mdbook-admonish.css"]
|
||||
4
book/src/SUMMARY.md
Normal file
4
book/src/SUMMARY.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Summary
|
||||
|
||||
- [Overview](./overview.md)
|
||||
- [Reference](./reference.md)
|
||||
166
book/src/overview.md
Normal file
166
book/src/overview.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# mdbook-admonish
|
||||
|
||||
[](https://crates.io/crates/mdbook-admonish)
|
||||
[](https://docs.rs/mdbook-admonish)
|
||||
|
||||
A preprocessor for [mdbook](https://github.com/rust-lang-nursery/mdBook) to add [Material Design](https://material.io/design) admonishments, based on the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) implementation.
|
||||
|
||||
It turns this:
|
||||
|
||||
````
|
||||
```admonish info
|
||||
A beautifully styled message.
|
||||
```
|
||||
````
|
||||
|
||||
into this:
|
||||
|
||||
```admonish info
|
||||
A beautifully styled message.
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
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>`:
|
||||
|
||||
````
|
||||
```admonish example
|
||||
My example is the best!
|
||||
```
|
||||
````
|
||||
|
||||
```admonish example
|
||||
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:
|
||||
|
||||
- `info`
|
||||
- `warning`
|
||||
- `danger`
|
||||
- `example`
|
||||
|
||||
and quite a few more!
|
||||
|
||||
You can also leave out the admonition type altogether, in which case it will default to `note`:
|
||||
|
||||
````
|
||||
```admonish
|
||||
A plain note.
|
||||
```
|
||||
````
|
||||
|
||||
```admonish
|
||||
A plain note.
|
||||
```
|
||||
|
||||
### Additional Options
|
||||
|
||||
#### Custom title
|
||||
|
||||
A custom title can be provided, contained in a double quoted TOML string.
|
||||
Note that TOML escapes must be escaped again - for instance, write `\"` as `\\"`.
|
||||
|
||||
````
|
||||
```admonish warning title="Data loss"
|
||||
The following steps can lead to irrecoverable data corruption.
|
||||
```
|
||||
````
|
||||
|
||||
```admonish warning title="Data loss"
|
||||
The following steps can lead to irrecoverable data corruption.
|
||||
```
|
||||
|
||||
You can also remove the title bar entirely, by specifying the empty string:
|
||||
|
||||
````
|
||||
```admonish success title=""
|
||||
This will take a while, go and grab a drink of water.
|
||||
```
|
||||
````
|
||||
|
||||
```admonish success title=""
|
||||
This will take a while, go and grab a drink of water.
|
||||
```
|
||||
|
||||
#### Nested Markdown/HTML
|
||||
|
||||
Markdown and HTML can be used in the inner content, as you'd expect:
|
||||
|
||||
````
|
||||
```admonish tip title="_Referencing_ and <i>dereferencing</i>"
|
||||
The opposite of *referencing* by using `&` is *dereferencing*, which is
|
||||
accomplished with the <span style="color: hotpink">dereference operator</span>, `*`.
|
||||
```
|
||||
````
|
||||
|
||||
```admonish tip title="_Referencing_ and <i>dereferencing</i>"
|
||||
The opposite of *referencing* by using `&` is *dereferencing*, which is
|
||||
accomplished with the <span style="color: hotpink">dereference operator</span>, `*`.
|
||||
```
|
||||
|
||||
If you have code blocks you want to include in the content, use [tildes for the outer code fence](https://spec.commonmark.org/0.30/#fenced-code-blocks):
|
||||
|
||||
````
|
||||
~~~admonish bug
|
||||
This syntax won't work in Python 3:
|
||||
```python
|
||||
print "Hello, world!"
|
||||
```
|
||||
~~~
|
||||
````
|
||||
|
||||
```admonish bug
|
||||
This syntax won't work in Python 3:
|
||||
~~~python
|
||||
print "Hello, world!"
|
||||
~~~
|
||||
```
|
||||
|
||||
#### Custom styling
|
||||
|
||||
If you want to provide custom styling to a specific admonition, you can attach one or more custom classnames:
|
||||
|
||||
````
|
||||
```admonish note class="custom-0 custom-1"
|
||||
Styled with my custom CSS class.
|
||||
```
|
||||
````
|
||||
|
||||
Will yield something like the following HTML, which you can then apply styles to:
|
||||
|
||||
```html
|
||||
<div class="admonition note custom-0 custom-1"
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Collapsible
|
||||
|
||||
For a block to be initially collapsible, and then be openable, set `collapsible=true`:
|
||||
|
||||
````
|
||||
```admonish collapsible=true
|
||||
Content will be hidden initially.
|
||||
```
|
||||
````
|
||||
|
||||
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
|
||||
```
|
||||
77
book/src/reference.md
Normal file
77
book/src/reference.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Reference
|
||||
|
||||
## Directives
|
||||
|
||||
All supported directives are listed below.
|
||||
|
||||
`note`
|
||||
|
||||
```admonish note
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`abstract`, `summary`, `tldr`
|
||||
|
||||
```admonish abstract
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`info`, `todo`
|
||||
|
||||
```admonish info
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`tip`, `hint`, `important`
|
||||
|
||||
```admonish tip
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`success`, `check`, `done`
|
||||
|
||||
```admonish success
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`question`, `help`, `faq`
|
||||
|
||||
```admonish question
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`warning`, `caution`, `attention`
|
||||
|
||||
```admonish warning
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`failure`, `fail`, `missing`
|
||||
|
||||
```admonish failure
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`danger`, `error`
|
||||
|
||||
```admonish danger
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`bug`
|
||||
|
||||
```admonish bug
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`example`
|
||||
|
||||
```admonish example
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
|
||||
`quote`, `cite`
|
||||
|
||||
```admonish quote
|
||||
Rust is a multi-paradigm, general-purpose programming language designed for performance and safety, especially safe concurrency.
|
||||
```
|
||||
@@ -65,6 +65,8 @@ $admonitions: (
|
||||
--md-admonition-icon--#{nth($names, 1)}:
|
||||
url("data:image/svg+xml;charset=utf-8,#{nth($props, 2)}");
|
||||
}
|
||||
--md-details-icon:
|
||||
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -114,23 +116,46 @@ $admonitions: (
|
||||
}
|
||||
}
|
||||
|
||||
// Anchor links
|
||||
a.admonition-anchor-link {
|
||||
// Don't display the link by default
|
||||
display: none;
|
||||
|
||||
// Position to the left of the element to link to
|
||||
position: absolute;
|
||||
left: -1.2rem;
|
||||
// Ensure we have enough padding, so that we can move the mouse to click on it
|
||||
padding-right: 1.0rem;
|
||||
|
||||
&:link, &:visited {
|
||||
// Don't make links colored (override to standard text color)
|
||||
// variable provided downstream by mdbook
|
||||
color: var(--fg);
|
||||
}
|
||||
&:link:hover, &:visited:hover {
|
||||
// No underline on hover
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '§';
|
||||
}
|
||||
}
|
||||
|
||||
// Admonition title
|
||||
:is(.admonition-title, summary) {
|
||||
position: relative;
|
||||
margin-block: 0;
|
||||
margin-inline: -1.6rem -1.2rem;
|
||||
padding-block: 0.8rem;
|
||||
padding-inline: 4rem 1.2rem;
|
||||
padding-inline: 4.4rem 1.2rem;
|
||||
font-weight: 700;
|
||||
background-color: color.adjust($clr-blue-a200, $alpha: -0.9);
|
||||
border: 0 solid $clr-blue-a200;
|
||||
border-inline-start-width: 0.4rem;
|
||||
border-start-start-radius: 0.2rem;
|
||||
// Compatilility with rendering markdown inside the content
|
||||
display: flex;
|
||||
|
||||
// Compatilility with rendering markdown inside the content
|
||||
& > p {
|
||||
& p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -143,7 +168,7 @@ $admonitions: (
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-start: 1.2rem;
|
||||
inset-inline-start: 1.6rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: $clr-blue-a200;
|
||||
@@ -155,6 +180,36 @@ $admonitions: (
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
}
|
||||
|
||||
|
||||
// Show anchor link on hover over title
|
||||
&:hover a.admonition-anchor-link {
|
||||
display: initial
|
||||
}
|
||||
}
|
||||
|
||||
summary.admonition-title {
|
||||
details.admonition > &::after {
|
||||
position: absolute;
|
||||
top: .625em;
|
||||
inset-inline-end: 1.6rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
background-color: currentcolor;
|
||||
mask-image: var(--md-details-icon);
|
||||
-webkit-mask-image: var(--md-details-icon);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
transform: rotate(0deg);
|
||||
transition: transform .25s;
|
||||
}
|
||||
|
||||
details[open].admonition > &::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -179,7 +234,6 @@ $admonitions: (
|
||||
// Admonition flavour title
|
||||
:is(#{$flavours}) > :is(.admonition-title, summary) {
|
||||
background-color: color.adjust($tint, $alpha: -0.9);
|
||||
border-color: $tint;
|
||||
|
||||
// Admonition icon
|
||||
&::before {
|
||||
@@ -217,4 +271,9 @@ $admonitions: (
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
& .admonition-anchor-link {
|
||||
&:link, &:visited {
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
img/no-title-bar.png
Normal file
BIN
img/no-title-bar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
2
integration/.gitignore
vendored
Normal file
2
integration/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/book
|
||||
/mdbook-admonish.css
|
||||
17
integration/book.toml
Normal file
17
integration/book.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[book]
|
||||
authors = ["Tom Milligan"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "mdbook-admonish-integration"
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["././mdbook-admonish.css"]
|
||||
17
integration/expected/book.toml
Normal file
17
integration/expected/book.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[book]
|
||||
authors = ["Tom Milligan"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "mdbook-admonish-integration"
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["././mdbook-admonish.css"]
|
||||
49
integration/expected/chapter_1_main.html
Normal file
49
integration/expected/chapter_1_main.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>
|
||||
<div id="admonition-what-is-this" class="admonition abstract">
|
||||
<div class="admonition-title">
|
||||
<p>What <i>is</i> this?</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-what-is-this"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<p>This book acts as an integration test for <code>mdbook-admonish</code>.</p>
|
||||
<p>It verifies that <code>mdbook</code> post-processes our generated HTML in the way we expect.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<p>Note</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Simples</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-default" class="admonition warning">
|
||||
<div>
|
||||
<p>No title, only body</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-error-rendering-admonishment" class="admonition bug">
|
||||
<div class="admonition-title">
|
||||
<p>Error rendering admonishment</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Failed with: TOML parsing error: unterminated string at line 1 column 7</p>
|
||||
<p>Original markdown input:</p>
|
||||
<pre><code>```admonish title="
|
||||
No title, only body
|
||||
```
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<details id="admonition-note-1" class="admonition note">
|
||||
<summary class="admonition-title">
|
||||
<p>Note</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note-1"></a></p>
|
||||
</summary>
|
||||
<div>
|
||||
<p>Hidden on load</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
57
integration/scripts/check
Executable file
57
integration/scripts/check
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
function eprintln() {
|
||||
>&2 echo "$1"
|
||||
}
|
||||
|
||||
pushd ..
|
||||
eprintln "Installing mdbook-admonish (system)"
|
||||
cargo install --path . --force
|
||||
popd
|
||||
|
||||
eprintln "Installing mdbook-admonish (book)"
|
||||
mdbook-admonish install .
|
||||
|
||||
eprintln "Verifying generated book config"
|
||||
set +e
|
||||
diff -u \
|
||||
"expected/book.toml" \
|
||||
"./book.toml"
|
||||
DIFF_RESULT=$?
|
||||
set -e
|
||||
|
||||
if [ "$DIFF_RESULT" != 0 ]; then
|
||||
eprintln ""
|
||||
eprintln "error: generated book config was different than expected"
|
||||
eprintln ""
|
||||
eprintln "error: If you expected the output to change, run:"
|
||||
eprintln "cp ./integration/book.toml ./integration/expected/book.toml"
|
||||
eprintln "and commit the result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eprintln "Building mdbook"
|
||||
mdbook build
|
||||
|
||||
|
||||
eprintln "Verifying generated html"
|
||||
set +e
|
||||
diff -u \
|
||||
"expected/chapter_1_main.html" \
|
||||
<(./scripts/get-snapshot)
|
||||
DIFF_RESULT=$?
|
||||
set -e
|
||||
|
||||
if [ "$DIFF_RESULT" != 0 ]; then
|
||||
eprintln ""
|
||||
eprintln "error: generated html was different than expected"
|
||||
eprintln ""
|
||||
eprintln "error: If you expected the output to change, run:"
|
||||
eprintln "./integration/update-snapshot"
|
||||
eprintln "and commit the result"
|
||||
exit 1
|
||||
fi
|
||||
7
integration/scripts/get-snapshot
Executable file
7
integration/scripts/get-snapshot
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
sed '1,/<main>/d;/<\/main>/,$d' book/chapter_1.html
|
||||
7
integration/scripts/update-snapshot
Executable file
7
integration/scripts/update-snapshot
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
./scripts/get-snapshot > expected/chapter_1_main.html
|
||||
3
integration/src/SUMMARY.md
Normal file
3
integration/src/SUMMARY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Summary
|
||||
|
||||
- [Chapter 1](./chapter_1.md)
|
||||
23
integration/src/chapter_1.md
Normal file
23
integration/src/chapter_1.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Chapter 1
|
||||
|
||||
```admonish abstract "What <i>is</i> this?"
|
||||
This book acts as an integration test for `mdbook-admonish`.
|
||||
|
||||
It verifies that `mdbook` post-processes our generated HTML in the way we expect.
|
||||
```
|
||||
|
||||
```admonish
|
||||
Simples
|
||||
```
|
||||
|
||||
```admonish warning ""
|
||||
No title, only body
|
||||
```
|
||||
|
||||
```admonish title="
|
||||
No title, only body
|
||||
```
|
||||
|
||||
```admonish collapsible=true
|
||||
Hidden on load
|
||||
```
|
||||
24
scripts/build-book
Executable file
24
scripts/build-book
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build book, using styles present in the repository.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
function eprintln() {
|
||||
>&2 echo "$1"
|
||||
}
|
||||
|
||||
eprintln "Installing mdbook-admonish (to system)"
|
||||
cargo install --path . --force
|
||||
|
||||
pushd book
|
||||
eprintln "Installing mdbook-admonish (to book)"
|
||||
mdbook-admonish install .
|
||||
|
||||
eprintln "Building book"
|
||||
mdbook build
|
||||
popd
|
||||
|
||||
eprintln "Book generated at ./book/book/index.html"
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
function eprintln() {
|
||||
>&2 echo "$1"
|
||||
}
|
||||
@@ -13,10 +15,12 @@ cargo fmt -- --check
|
||||
# - 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"
|
||||
@@ -30,4 +34,7 @@ eprintln "Running tests (cli)"
|
||||
cargo test --no-default-features --features cli
|
||||
|
||||
eprintln "Building documentation"
|
||||
cargo doc --no-deps
|
||||
cargo doc --no-deps --lib
|
||||
|
||||
eprintln "Running mdbook integration test"
|
||||
./integration/scripts/check
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Install everything for development/CI
|
||||
#
|
||||
# Does not include offline node development stack (i.e. yarn)
|
||||
|
||||
set -exuo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
rustup component add rustfmt clippy
|
||||
cargo install cargo-audit
|
||||
|
||||
if ! cargo audit --version; then
|
||||
cargo install cargo-audit --force
|
||||
fi
|
||||
|
||||
./scripts/install-mdbook
|
||||
|
||||
9
scripts/install-mdbook
Executable file
9
scripts/install-mdbook
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -exuo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
if ! mdbook --version; then
|
||||
cargo install mdbook --force
|
||||
fi
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
# Don't log the cargo login token while authenticating
|
||||
set +x
|
||||
echo "cargo login ***********************************"
|
||||
|
||||
18
scripts/rebuild-book
Executable file
18
scripts/rebuild-book
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Development only. Rebuilds the book, including recompiling styles.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
function eprintln() {
|
||||
>&2 echo "$1"
|
||||
}
|
||||
|
||||
eprintln "Generating styles"
|
||||
pushd compile_assets
|
||||
yarn run build
|
||||
popd
|
||||
|
||||
./scripts/build-book
|
||||
1
src/REQUIRED_ASSETS_VERSION
Normal file
1
src/REQUIRED_ASSETS_VERSION
Normal file
@@ -0,0 +1 @@
|
||||
^2.0.0
|
||||
1
src/bin/assets/VERSION
Normal file
1
src/bin/assets/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
2.0.0
|
||||
@@ -1,3 +1,4 @@
|
||||
@charset "UTF-8";
|
||||
:root {
|
||||
--md-admonition-icon--note:
|
||||
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
|
||||
@@ -23,6 +24,8 @@
|
||||
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z'/></svg>");
|
||||
--md-admonition-icon--quote:
|
||||
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>");
|
||||
--md-details-icon:
|
||||
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
|
||||
}
|
||||
|
||||
:is(.admonition) {
|
||||
@@ -56,20 +59,33 @@ html :is(.admonition) > :last-child {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
a.admonition-anchor-link {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: -1.2rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
a.admonition-anchor-link:link, a.admonition-anchor-link:visited {
|
||||
color: var(--fg);
|
||||
}
|
||||
a.admonition-anchor-link:link:hover, a.admonition-anchor-link:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
a.admonition-anchor-link::before {
|
||||
content: "§";
|
||||
}
|
||||
|
||||
:is(.admonition-title, summary) {
|
||||
position: relative;
|
||||
margin-block: 0;
|
||||
margin-inline: -1.6rem -1.2rem;
|
||||
padding-block: 0.8rem;
|
||||
padding-inline: 4rem 1.2rem;
|
||||
padding-inline: 4.4rem 1.2rem;
|
||||
font-weight: 700;
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
border: 0 solid #448aff;
|
||||
border-inline-start-width: 0.4rem;
|
||||
border-start-start-radius: 0.2rem;
|
||||
display: flex;
|
||||
}
|
||||
:is(.admonition-title, summary) > p {
|
||||
:is(.admonition-title, summary) p {
|
||||
margin: 0;
|
||||
}
|
||||
html :is(.admonition-title, summary):last-child {
|
||||
@@ -78,7 +94,7 @@ html :is(.admonition-title, summary):last-child {
|
||||
:is(.admonition-title, summary)::before {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-start: 1.2rem;
|
||||
inset-inline-start: 1.6rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: #448aff;
|
||||
@@ -90,6 +106,30 @@ html :is(.admonition-title, summary):last-child {
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
}
|
||||
:is(.admonition-title, summary):hover a.admonition-anchor-link {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
details.admonition > summary.admonition-title::after {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-end: 1.6rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
background-color: currentcolor;
|
||||
mask-image: var(--md-details-icon);
|
||||
-webkit-mask-image: var(--md-details-icon);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
details[open].admonition > summary.admonition-title::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
:is(.admonition):is(.note) {
|
||||
border-color: #448aff;
|
||||
@@ -97,7 +137,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.note) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
border-color: #448aff;
|
||||
}
|
||||
:is(.note) > :is(.admonition-title, summary)::before {
|
||||
background-color: #448aff;
|
||||
@@ -115,7 +154,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(0, 176, 255, 0.1);
|
||||
border-color: #00b0ff;
|
||||
}
|
||||
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary)::before {
|
||||
background-color: #00b0ff;
|
||||
@@ -133,7 +171,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.info, .todo) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(0, 184, 212, 0.1);
|
||||
border-color: #00b8d4;
|
||||
}
|
||||
:is(.info, .todo) > :is(.admonition-title, summary)::before {
|
||||
background-color: #00b8d4;
|
||||
@@ -151,7 +188,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.tip, .hint, .important) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(0, 191, 165, 0.1);
|
||||
border-color: #00bfa5;
|
||||
}
|
||||
:is(.tip, .hint, .important) > :is(.admonition-title, summary)::before {
|
||||
background-color: #00bfa5;
|
||||
@@ -169,7 +205,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.success, .check, .done) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(0, 200, 83, 0.1);
|
||||
border-color: #00c853;
|
||||
}
|
||||
:is(.success, .check, .done) > :is(.admonition-title, summary)::before {
|
||||
background-color: #00c853;
|
||||
@@ -187,7 +222,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.question, .help, .faq) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(100, 221, 23, 0.1);
|
||||
border-color: #64dd17;
|
||||
}
|
||||
:is(.question, .help, .faq) > :is(.admonition-title, summary)::before {
|
||||
background-color: #64dd17;
|
||||
@@ -205,7 +239,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.warning, .caution, .attention) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(255, 145, 0, 0.1);
|
||||
border-color: #ff9100;
|
||||
}
|
||||
:is(.warning, .caution, .attention) > :is(.admonition-title, summary)::before {
|
||||
background-color: #ff9100;
|
||||
@@ -223,7 +256,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.failure, .fail, .missing) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(255, 82, 82, 0.1);
|
||||
border-color: #ff5252;
|
||||
}
|
||||
:is(.failure, .fail, .missing) > :is(.admonition-title, summary)::before {
|
||||
background-color: #ff5252;
|
||||
@@ -241,7 +273,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.danger, .error) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(255, 23, 68, 0.1);
|
||||
border-color: #ff1744;
|
||||
}
|
||||
:is(.danger, .error) > :is(.admonition-title, summary)::before {
|
||||
background-color: #ff1744;
|
||||
@@ -259,7 +290,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.bug) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(245, 0, 87, 0.1);
|
||||
border-color: #f50057;
|
||||
}
|
||||
:is(.bug) > :is(.admonition-title, summary)::before {
|
||||
background-color: #f50057;
|
||||
@@ -277,7 +307,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.example) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(124, 77, 255, 0.1);
|
||||
border-color: #7c4dff;
|
||||
}
|
||||
:is(.example) > :is(.admonition-title, summary)::before {
|
||||
background-color: #7c4dff;
|
||||
@@ -295,7 +324,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
|
||||
:is(.quote, .cite) > :is(.admonition-title, summary) {
|
||||
background-color: rgba(158, 158, 158, 0.1);
|
||||
border-color: #9e9e9e;
|
||||
}
|
||||
:is(.quote, .cite) > :is(.admonition-title, summary)::before {
|
||||
background-color: #9e9e9e;
|
||||
@@ -319,3 +347,6 @@ html :is(.admonition-title, summary):last-child {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.rust .admonition-anchor-link:link, .rust .admonition-anchor-link:visited {
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,70 @@
|
||||
use clap::{crate_version, Arg, ArgMatches, Command};
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use mdbook::{
|
||||
errors::Error,
|
||||
preprocess::{CmdPreprocessor, Preprocessor},
|
||||
};
|
||||
use mdbook_admonish::Admonish;
|
||||
|
||||
#[cfg(feature = "cli-install")]
|
||||
use std::path::PathBuf;
|
||||
use std::{io, process};
|
||||
|
||||
pub fn make_app() -> Command<'static> {
|
||||
let mut command = Command::new("mdbook-admonish")
|
||||
.version(crate_version!())
|
||||
.about("mdbook preprocessor to add support for admonitions");
|
||||
command = command.subcommand(
|
||||
Command::new("supports")
|
||||
.arg(Arg::new("renderer").required(true))
|
||||
.about("Check whether a renderer is supported by this preprocessor"),
|
||||
);
|
||||
/// mdbook preprocessor to add support for admonitions
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Check whether a renderer is supported by this preprocessor
|
||||
Supports { renderer: String },
|
||||
|
||||
#[cfg(feature = "cli-install")]
|
||||
{
|
||||
command = command.subcommand(
|
||||
Command::new("install")
|
||||
.arg(Arg::new("css-dir").long("css-dir").default_value(".").help(
|
||||
"Relative directory for the css assets,\nfrom the book directory root",
|
||||
))
|
||||
.arg(Arg::new("dir").default_value(".").help(
|
||||
"Root directory for the book,\nshould contain the configuration file (`book.toml`)",
|
||||
))
|
||||
.about("Install the required assset files and include it in the config"));
|
||||
}
|
||||
command
|
||||
/// Install the required assset files and include it in the config
|
||||
Install {
|
||||
/// Root directory for the book, should contain the configuration file (`book.toml`)
|
||||
///
|
||||
/// If not set, defaults to the current directory.
|
||||
dir: Option<PathBuf>,
|
||||
|
||||
/// Relative directory for the css assets, from the book directory root
|
||||
///
|
||||
/// If not set, defaults to the current directory.
|
||||
#[arg(long)]
|
||||
css_dir: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
|
||||
|
||||
let matches = make_app().get_matches();
|
||||
|
||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||
handle_supports(sub_args);
|
||||
} else if let Some(sub_args) = matches.subcommand_matches("install") {
|
||||
#[cfg(feature = "cli-install")]
|
||||
{
|
||||
install::handle_install(sub_args);
|
||||
let cli = Cli::parse();
|
||||
if let Err(error) = run(cli) {
|
||||
log::error!("Fatal error: {}", error);
|
||||
for error in error.chain() {
|
||||
log::error!(" - {}", error);
|
||||
}
|
||||
#[cfg(not(feature = "cli-install"))]
|
||||
{
|
||||
panic!("cli-install feature not enabled: {:?}", sub_args)
|
||||
}
|
||||
} else if let Err(e) = handle_preprocessing() {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(cli: Cli) -> Result<()> {
|
||||
match cli.command {
|
||||
None => handle_preprocessing(),
|
||||
Some(Commands::Supports { renderer }) => {
|
||||
handle_supports(renderer);
|
||||
}
|
||||
#[cfg(feature = "cli-install")]
|
||||
Some(Commands::Install { dir, css_dir }) => install::handle_install(
|
||||
dir.unwrap_or_else(|| PathBuf::from(".")),
|
||||
css_dir.unwrap_or_else(|| PathBuf::from(".")),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_preprocessing() -> Result<(), Error> {
|
||||
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
||||
|
||||
@@ -70,9 +83,8 @@ fn handle_preprocessing() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_supports(sub_args: &ArgMatches) -> ! {
|
||||
let renderer = sub_args.value_of("renderer").expect("Required argument");
|
||||
let supported = Admonish.supports_renderer(renderer);
|
||||
fn handle_supports(renderer: String) -> ! {
|
||||
let supported = Admonish.supports_renderer(&renderer);
|
||||
|
||||
// Signal whether the renderer is supported by exiting with 1 or 0.
|
||||
if supported {
|
||||
@@ -84,12 +96,11 @@ fn handle_supports(sub_args: &ArgMatches) -> ! {
|
||||
|
||||
#[cfg(feature = "cli-install")]
|
||||
mod install {
|
||||
use clap::ArgMatches;
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
process,
|
||||
};
|
||||
use toml_edit::{self, Array, Document, Item, Table, Value};
|
||||
|
||||
@@ -111,31 +122,30 @@ mod install {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_install(sub_args: &ArgMatches) {
|
||||
let dir = sub_args.value_of("dir").expect("Required argument");
|
||||
let css_dir = sub_args.value_of("css-dir").expect("Required argument");
|
||||
let proj_dir = PathBuf::from(dir);
|
||||
pub fn handle_install(proj_dir: PathBuf, css_dir: PathBuf) -> Result<()> {
|
||||
let config = proj_dir.join("book.toml");
|
||||
|
||||
if !config.exists() {
|
||||
log::error!("Configuration file '{}' missing", config.display());
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
log::info!("Reading configuration file '{}'", config.display());
|
||||
let toml = fs::read_to_string(&config).expect("can't read configuration file");
|
||||
let toml = fs::read_to_string(&config)
|
||||
.with_context(|| format!("can't read configuration file '{}'", config.display()))?;
|
||||
let mut doc = toml
|
||||
.parse::<Document>()
|
||||
.expect("configuration is not valid TOML");
|
||||
.context("configuration is not valid TOML")?;
|
||||
|
||||
if preprocessor(&mut doc).is_err() {
|
||||
if let Ok(preprocessor) = preprocessor(&mut doc) {
|
||||
const ASSETS_VERSION: &str = std::include_str!("./assets/VERSION");
|
||||
let value = toml_edit::value(
|
||||
toml_edit::Value::from(ASSETS_VERSION.trim())
|
||||
.decorated(" ", " # do not edit: managed by `mdbook-admonish install`"),
|
||||
);
|
||||
preprocessor["assets_version"] = value;
|
||||
} else {
|
||||
log::info!("Unexpected configuration, not updating prereprocessor configuration");
|
||||
};
|
||||
|
||||
let mut additional_css = additional_css(&mut doc);
|
||||
for (name, content) in ADMONISH_CSS_FILES {
|
||||
let filepath = proj_dir.join(css_dir).join(name);
|
||||
let filepath_str = filepath.to_str().expect("non-utf8 filepath");
|
||||
let filepath = proj_dir.join(&css_dir).join(name);
|
||||
let filepath_str = filepath.to_str().context("non-utf8 filepath")?;
|
||||
|
||||
if let Ok(ref mut additional_css) = additional_css {
|
||||
if !additional_css.contains_str(filepath_str) {
|
||||
@@ -150,18 +160,18 @@ mod install {
|
||||
"Copying '{name}' to '{filepath}'",
|
||||
filepath = filepath.display()
|
||||
);
|
||||
let mut file = File::create(filepath).expect("can't open file for writing");
|
||||
let mut file = File::create(&filepath).context("can't open file for writing")?;
|
||||
file.write_all(content)
|
||||
.expect("can't write content to file");
|
||||
.context("can't write content to file")?;
|
||||
}
|
||||
|
||||
let new_toml = doc.to_string();
|
||||
if new_toml != toml {
|
||||
log::info!("Saving changed configuration to '{}'", config.display());
|
||||
let mut file =
|
||||
File::create(config).expect("can't open configuration file for writing.");
|
||||
File::create(config).context("can't open configuration file for writing.")?;
|
||||
file.write_all(new_toml.as_bytes())
|
||||
.expect("can't write configuration");
|
||||
.context("can't write configuration")?;
|
||||
} else {
|
||||
log::info!("Configuration '{}' already up to date", config.display());
|
||||
}
|
||||
@@ -171,8 +181,7 @@ mod install {
|
||||
A beautifully styled message.
|
||||
```"#;
|
||||
log::info!("Add a code block like:\n{}", codeblock);
|
||||
|
||||
process::exit(0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the `additional-css` field, initializing if required.
|
||||
@@ -187,7 +196,7 @@ A beautifully styled message.
|
||||
doc.entry("output")
|
||||
.or_insert(empty_table.clone())
|
||||
.as_table_mut()
|
||||
.map(|item| {
|
||||
.and_then(|item| {
|
||||
item.entry("html")
|
||||
.or_insert(empty_table)
|
||||
.as_table_mut()?
|
||||
@@ -196,7 +205,6 @@ A beautifully styled message.
|
||||
.as_value_mut()?
|
||||
.as_array_mut()
|
||||
})
|
||||
.flatten()
|
||||
.ok_or(())
|
||||
}
|
||||
|
||||
|
||||
158
src/config/mod.rs
Normal file
158
src/config/mod.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use crate::types::Directive;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod v1;
|
||||
mod v2;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
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.
|
||||
fn admonition_config_string(info_string: &str) -> Option<&str> {
|
||||
const ADMONISH_BLOCK_KEYWORD: &str = "admonish";
|
||||
|
||||
// Get the rest of the info string if this is an admonition
|
||||
if info_string == ADMONISH_BLOCK_KEYWORD {
|
||||
return Some("");
|
||||
}
|
||||
|
||||
match info_string.split_once(' ') {
|
||||
Some((keyword, rest)) if keyword == ADMONISH_BLOCK_KEYWORD => Some(rest),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl AdmonitionInfoRaw {
|
||||
/// Returns:
|
||||
/// - `None` if this is not 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)?;
|
||||
|
||||
// If we succeed at parsing v2, return that. Otherwise hold onto the error
|
||||
let config_v2_error = match v2::from_config_string(config_string) {
|
||||
Ok(config) => return Some(Ok(config)),
|
||||
Err(config) => config,
|
||||
};
|
||||
|
||||
Some(
|
||||
if let Ok(info_raw) = v1::from_config_string(config_string) {
|
||||
// If we succeed at parsing v1, return that.
|
||||
Ok(info_raw)
|
||||
} else {
|
||||
// Otherwise return our v2 error.
|
||||
Err(config_v2_error)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct AdmonitionInfo {
|
||||
pub directive: Directive,
|
||||
pub title: Option<String>,
|
||||
pub additional_classnames: Vec<String>,
|
||||
pub collapsible: bool,
|
||||
}
|
||||
|
||||
impl AdmonitionInfo {
|
||||
pub fn from_info_string(info_string: &str) -> Option<Result<Self, String>> {
|
||||
AdmonitionInfoRaw::from_info_string(info_string).map(|result| result.map(Into::into))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdmonitionInfoRaw> for AdmonitionInfo {
|
||||
fn from(other: AdmonitionInfoRaw) -> Self {
|
||||
let AdmonitionInfoRaw {
|
||||
directive: raw_directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
} = other;
|
||||
let (directive, title) = match (Directive::from_str(&raw_directive), title) {
|
||||
(Ok(directive), None) => (directive, ucfirst(&raw_directive)),
|
||||
(Err(_), None) => (Directive::Note, "Note".to_owned()),
|
||||
(Ok(directive), Some(title)) => (directive, title),
|
||||
(Err(_), Some(title)) => (Directive::Note, title),
|
||||
};
|
||||
// If the user explicitly gave no title, then disable the title bar
|
||||
let title = if title.is_empty() { None } else { Some(title) };
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the first letter of `input` upppercase.
|
||||
///
|
||||
/// source: https://stackoverflow.com/a/38406885
|
||||
fn ucfirst(input: &str) -> String {
|
||||
let mut chars = input.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_from_info_string() {
|
||||
// Not admonition blocks
|
||||
assert_eq!(AdmonitionInfoRaw::from_info_string(""), None);
|
||||
assert_eq!(AdmonitionInfoRaw::from_info_string("adm"), None);
|
||||
// v1 syntax is supported back compatibly
|
||||
assert_eq!(
|
||||
AdmonitionInfoRaw::from_info_string("admonish note.additional-classname")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: vec!["additional-classname".to_owned()],
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
// v2 syntax is supported
|
||||
assert_eq!(
|
||||
AdmonitionInfoRaw::from_info_string(r#"admonish title="Custom Title" type="question""#)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "question".to_owned(),
|
||||
title: Some("Custom Title".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw() {
|
||||
assert_eq!(
|
||||
AdmonitionInfo::from(AdmonitionInfoRaw {
|
||||
directive: " ".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}),
|
||||
AdmonitionInfo {
|
||||
directive: Directive::Note,
|
||||
title: Some("Note".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
130
src/config/v1.rs
Normal file
130
src/config/v1.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use super::AdmonitionInfoRaw;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
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(|| {
|
||||
let directive = r#"[a-z]+"#;
|
||||
let css_classname = r#"-?[_a-zA-Z]+[_a-zA-Z0-9-]*"#;
|
||||
let title = r#"".*""#;
|
||||
Regex::new(&format!(
|
||||
"^({directive})?(\\.({css_classname})?)*( {title})?$"
|
||||
))
|
||||
.expect("config string v1 regex")
|
||||
});
|
||||
|
||||
// Check if this is a valid looking v1 directive
|
||||
if !RX_CONFIG_STRING_V1.is_match(config_string) {
|
||||
return Err("Invalid configuration string".to_owned());
|
||||
}
|
||||
|
||||
// If we're just given the directive, handle that
|
||||
let (directive, title) = config_string
|
||||
.split_once(' ')
|
||||
.map(|(directive, title)| (directive, Some(title)))
|
||||
.unwrap_or_else(|| (config_string, None));
|
||||
|
||||
// The title is expected to be a quoted JSON string
|
||||
// If parsing fails, output the error message as the title for the user to correct
|
||||
let title = title
|
||||
.map(|title| {
|
||||
serde_json::from_str::<String>(title)
|
||||
.map_err(|error| format!("Error parsing JSON string: {error}"))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// If the directive contains additional classes, parse them out
|
||||
const CLASSNAME_SEPARATOR: char = '.';
|
||||
let (directive, additional_classnames) = match directive.split_once(CLASSNAME_SEPARATOR) {
|
||||
None => (directive, Vec::new()),
|
||||
Some((directive, additional_classnames)) => (
|
||||
directive,
|
||||
additional_classnames
|
||||
.split(CLASSNAME_SEPARATOR)
|
||||
.filter(|classname| !classname.is_empty())
|
||||
.map(|classname| classname.to_owned())
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
|
||||
Ok(AdmonitionInfoRaw {
|
||||
directive: directive.to_owned(),
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string() {
|
||||
assert_eq!(
|
||||
from_config_string("").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(" ").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("unknown").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "unknown".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("note").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("note.additional-classname").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: vec!["additional-classname".to_owned()],
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_invalid_title_json() {
|
||||
// Test invalid JSON title
|
||||
assert_eq!(
|
||||
from_config_string(r#"note "\""#).unwrap_err(),
|
||||
"Error parsing JSON string: EOF while parsing a string at line 1 column 3".to_owned()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_v2_format() {
|
||||
assert_eq!(
|
||||
from_config_string(r#"note title="Custom""#).unwrap_err(),
|
||||
"Invalid configuration string".to_owned()
|
||||
);
|
||||
}
|
||||
}
|
||||
172
src/config/v2.rs
Normal file
172
src/config/v2.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use super::AdmonitionInfoRaw;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct AdmonitionInfoConfig {
|
||||
#[serde(default)]
|
||||
r#type: Option<String>,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
#[serde(default)]
|
||||
class: Option<String>,
|
||||
#[serde(default)]
|
||||
collapsible: bool,
|
||||
}
|
||||
|
||||
/// Transform our config string into valid toml
|
||||
fn bare_key_value_pairs_to_toml(pairs: &str) -> String {
|
||||
use regex::Captures;
|
||||
|
||||
static RX_BARE_KEY_ASSIGNMENT: Lazy<Regex> = Lazy::new(|| {
|
||||
let bare_key = r#"[A-Za-z0-9_-]+"#;
|
||||
Regex::new(&format!("(?:{bare_key}) *=")).expect("bare key assignment regex")
|
||||
});
|
||||
|
||||
fn prefix_with_newline(captures: &Captures) -> String {
|
||||
format!(
|
||||
"\n{}",
|
||||
captures
|
||||
.get(0)
|
||||
.expect("capture to have group zero")
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
RX_BARE_KEY_ASSIGNMENT
|
||||
.replace_all(pairs, prefix_with_newline)
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Parse and return the config assuming v2 format.
|
||||
///
|
||||
/// Note that if an error occurs, a parsed struct that can be returned to
|
||||
/// show the error message will be returned.
|
||||
pub(crate) fn from_config_string(config_string: &str) -> Result<AdmonitionInfoRaw, String> {
|
||||
let config_toml = bare_key_value_pairs_to_toml(config_string);
|
||||
let config_toml = config_toml.trim();
|
||||
|
||||
let config: AdmonitionInfoConfig = match toml::from_str(config_toml) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
let original_error = Err(format!("TOML parsing error: {error}"));
|
||||
|
||||
// For ergonomic reasons, we allow users to specify the directive without
|
||||
// a key. So if parsing fails initially, take the first line,
|
||||
// use that as the directive, and reparse.
|
||||
let (directive, config_toml) = match config_toml.split_once('\n') {
|
||||
Some((directive, config_toml)) => (directive.trim(), config_toml),
|
||||
None => (config_toml, ""),
|
||||
};
|
||||
|
||||
static RX_DIRECTIVE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^[A-Za-z0-9_-]+$"#).expect("directive regex"));
|
||||
|
||||
if !RX_DIRECTIVE.is_match(directive) {
|
||||
return original_error;
|
||||
}
|
||||
|
||||
let mut config: AdmonitionInfoConfig = match toml::from_str(config_toml) {
|
||||
Ok(config) => config,
|
||||
Err(_) => return original_error,
|
||||
};
|
||||
config.r#type = Some(directive.to_owned());
|
||||
config
|
||||
}
|
||||
};
|
||||
let additional_classnames = config
|
||||
.class
|
||||
.map(|class| {
|
||||
class
|
||||
.split(' ')
|
||||
.filter(|classname| !classname.is_empty())
|
||||
.map(|classname| classname.to_owned())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Ok(AdmonitionInfoRaw {
|
||||
directive: config.r#type.unwrap_or_default(),
|
||||
title: config.title,
|
||||
additional_classnames,
|
||||
collapsible: config.collapsible,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_v2() {
|
||||
assert_eq!(
|
||||
from_config_string("").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(" ").unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
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: false,
|
||||
}
|
||||
);
|
||||
// Specifying unknown keys is okay, as long as they're valid
|
||||
assert_eq!(
|
||||
from_config_string(r#"unkonwn="but valid toml""#).unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
// Just directive is fine
|
||||
assert_eq!(
|
||||
from_config_string(r#"info"#).unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "info".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
// Directive plus toml config
|
||||
assert_eq!(
|
||||
from_config_string(r#"info title="Information""#).unwrap(),
|
||||
AdmonitionInfoRaw {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("Information".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
// Directive after toml config is an error
|
||||
assert!(from_config_string(r#"title="Information" info"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_invalid_toml_value() {
|
||||
assert_eq!(
|
||||
from_config_string(r#"note titlel=""#).unwrap_err(),
|
||||
"TOML parsing error: expected an equals, found a newline at line 1 column 6".to_owned()
|
||||
);
|
||||
}
|
||||
}
|
||||
682
src/lib.rs
682
src/lib.rs
@@ -1,9 +1,52 @@
|
||||
use mdbook::book::{Book, BookItem, Chapter};
|
||||
use mdbook::errors::Result as MdbookResult;
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
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;
|
||||
use std::str::FromStr;
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
|
||||
mod config;
|
||||
mod types;
|
||||
|
||||
use crate::{config::AdmonitionInfo, types::Directive};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum OnFailure {
|
||||
Bail,
|
||||
Continue,
|
||||
}
|
||||
|
||||
impl Default for OnFailure {
|
||||
fn default() -> Self {
|
||||
Self::Continue
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for OnFailure {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(string: &str) -> Result<Self, ()> {
|
||||
match string {
|
||||
"bail" => Ok(Self::Bail),
|
||||
"continue" => Ok(Self::Continue),
|
||||
_ => Ok(Self::Continue),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OnFailure {
|
||||
fn from_context(context: &PreprocessorContext) -> Self {
|
||||
context
|
||||
.config
|
||||
.get("preprocessor.admonish.on_failure")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(|value| OnFailure::from_str(value).unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Admonish;
|
||||
|
||||
@@ -12,7 +55,10 @@ impl Preprocessor for Admonish {
|
||||
"admonish"
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> MdbookResult<Book> {
|
||||
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 {
|
||||
@@ -20,7 +66,7 @@ impl Preprocessor for Admonish {
|
||||
}
|
||||
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
res = Some(Admonish::preprocess(chapter).map(|md| {
|
||||
res = Some(preprocess(&chapter.content, on_failure).map(|md| {
|
||||
chapter.content = md;
|
||||
}));
|
||||
}
|
||||
@@ -34,42 +80,42 @@ impl Preprocessor for Admonish {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Directive {
|
||||
Note,
|
||||
Abstract,
|
||||
Info,
|
||||
Tip,
|
||||
Success,
|
||||
Question,
|
||||
Warning,
|
||||
Failure,
|
||||
Danger,
|
||||
Bug,
|
||||
Example,
|
||||
Quote,
|
||||
}
|
||||
fn ensure_compatible_assets_version(ctx: &PreprocessorContext) -> Result<()> {
|
||||
use semver::{Version, VersionReq};
|
||||
|
||||
impl FromStr for Directive {
|
||||
type Err = ();
|
||||
const REQUIRES_ASSETS_VERSION: &str = std::include_str!("./REQUIRED_ASSETS_VERSION");
|
||||
let requirement = VersionReq::parse(REQUIRES_ASSETS_VERSION.trim()).unwrap();
|
||||
|
||||
fn from_str(string: &str) -> Result<Self, ()> {
|
||||
match string {
|
||||
"note" => Ok(Self::Note),
|
||||
"abstract" | "summary" | "tldr" => Ok(Self::Abstract),
|
||||
"info" | "todo" => Ok(Self::Info),
|
||||
"tip" | "hint" | "important" => Ok(Self::Tip),
|
||||
"success" | "check" | "done" => Ok(Self::Success),
|
||||
"question" | "help" | "faq" => Ok(Self::Question),
|
||||
"warning" | "caution" | "attention" => Ok(Self::Warning),
|
||||
"failure" | "fail" | "missing" => Ok(Self::Failure),
|
||||
"danger" | "error" => Ok(Self::Danger),
|
||||
"bug" => Ok(Self::Bug),
|
||||
"example" => Ok(Self::Example),
|
||||
"quote" | "cite" => Ok(Self::Quote),
|
||||
_ => Err(()),
|
||||
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 {
|
||||
@@ -91,137 +137,83 @@ impl Directive {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct AdmonitionInfoRaw<'a> {
|
||||
directive: &'a str,
|
||||
title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct AdmonitionInfo<'a> {
|
||||
directive: Directive,
|
||||
title: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl<'a> Default for AdmonitionInfo<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
directive: Directive::Note,
|
||||
title: Cow::Borrowed("Note"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<AdmonitionInfoRaw<'a>> for AdmonitionInfo<'a> {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(other: AdmonitionInfoRaw<'a>) -> Result<Self, ()> {
|
||||
let directive = Directive::from_str(other.directive)?;
|
||||
Ok(Self {
|
||||
directive,
|
||||
title: other
|
||||
.title
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|| Cow::Owned(ucfirst(other.directive))),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Admonition<'a> {
|
||||
directive: Directive,
|
||||
title: Cow<'a, str>,
|
||||
content: &'a str,
|
||||
title: Option<String>,
|
||||
content: Cow<'a, str>,
|
||||
additional_classnames: Vec<String>,
|
||||
collapsible: bool,
|
||||
}
|
||||
|
||||
impl<'a> Admonition<'a> {
|
||||
pub fn new(info: AdmonitionInfo<'a>, content: &'a str) -> Self {
|
||||
let AdmonitionInfo { directive, title } = info;
|
||||
pub fn new(info: AdmonitionInfo, content: &'a str) -> Self {
|
||||
let AdmonitionInfo {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
} = info;
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
content,
|
||||
content: Cow::Borrowed(content),
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
}
|
||||
}
|
||||
|
||||
fn html(&self) -> String {
|
||||
let directive_classname = self.directive.classname();
|
||||
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.
|
||||
// - <p> nested in <div> is deliberate
|
||||
// - If plain text is given, it is contained in the <p> tag
|
||||
// - If markdown is given, it is rendered into a new <p> tag.
|
||||
// This leads to it escaping the template <p> tag, and to apply
|
||||
// styling we contain in in the outer <div>.
|
||||
format!(
|
||||
r#"<div class="admonition {directive_classname}">
|
||||
<div class="admonition-title">
|
||||
<p>
|
||||
|
||||
{title}
|
||||
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
r#"<{admonition_block} id="{ANCHOR_ID_PREFIX}-{anchor_id}" class="admonition {additional_class}">
|
||||
{title_html}<div>
|
||||
|
||||
{content}
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>"#,
|
||||
</{admonition_block}>"#,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ADMONISH_BLOCK_KEYWORD: &str = "admonish";
|
||||
|
||||
/// Returns:
|
||||
/// - `None` if this is not an `admonish` block.
|
||||
/// - `Some(AdmonitionInfoRaw)` if this is an `admonish` block
|
||||
fn parse_info_string(info_string: &str) -> Option<AdmonitionInfoRaw> {
|
||||
let directive_title = if info_string == ADMONISH_BLOCK_KEYWORD {
|
||||
""
|
||||
} else {
|
||||
match info_string.split_once(' ') {
|
||||
Some((ADMONISH_BLOCK_KEYWORD, rest)) => rest,
|
||||
_ => return None,
|
||||
}
|
||||
};
|
||||
|
||||
let info = if let Some((directive, title)) = directive_title.split_once(' ') {
|
||||
// The title is expected to be a quoted JSON string
|
||||
let title: String = serde_json::from_str(title)
|
||||
.unwrap_or_else(|error| format!("Error parsing JSON string: {error}"));
|
||||
AdmonitionInfoRaw {
|
||||
directive,
|
||||
title: Some(title),
|
||||
}
|
||||
} else {
|
||||
AdmonitionInfoRaw {
|
||||
directive: directive_title,
|
||||
title: None,
|
||||
}
|
||||
};
|
||||
|
||||
Some(info)
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
}
|
||||
}
|
||||
const ANCHOR_ID_PREFIX: &str = "admonition";
|
||||
const ANCHOR_ID_DEFAULT: &str = "default";
|
||||
|
||||
fn extract_admonish_body(content: &str) -> &str {
|
||||
const PRE_END: char = '\n';
|
||||
@@ -248,15 +240,50 @@ fn extract_admonish_body(content: &str) -> &str {
|
||||
/// 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) -> Option<Admonition<'a>> {
|
||||
let info = parse_info_string(info_string)?;
|
||||
let info = AdmonitionInfo::try_from(info).unwrap_or_default();
|
||||
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(Admonition::new(info, body))
|
||||
Some(Ok(Admonition::new(info, body)))
|
||||
}
|
||||
|
||||
fn preprocess(content: &str) -> MdbookResult<String> {
|
||||
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);
|
||||
@@ -269,11 +296,17 @@ fn preprocess(content: &str) -> MdbookResult<String> {
|
||||
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) {
|
||||
let admonition = match parse_admonition(info_string.as_ref(), span_content, on_failure)
|
||||
{
|
||||
Some(admonition) => admonition,
|
||||
None => continue,
|
||||
};
|
||||
admonish_blocks.push((span, admonition.html()));
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,50 +319,13 @@ fn preprocess(content: &str) -> MdbookResult<String> {
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
impl Admonish {
|
||||
fn preprocess(chapter: &mut Chapter) -> MdbookResult<String> {
|
||||
preprocess(&chapter.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_info_string() {
|
||||
assert_eq!(parse_info_string(""), None);
|
||||
assert_eq!(parse_info_string("adm"), None);
|
||||
assert_eq!(
|
||||
parse_info_string("admonish"),
|
||||
Some(AdmonitionInfoRaw {
|
||||
directive: "",
|
||||
title: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse_info_string("admonish "),
|
||||
Some(AdmonitionInfoRaw {
|
||||
directive: "",
|
||||
title: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse_info_string("admonish unknown"),
|
||||
Some(AdmonitionInfoRaw {
|
||||
directive: "unknown",
|
||||
title: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse_info_string("admonish note"),
|
||||
Some(AdmonitionInfoRaw {
|
||||
directive: "note",
|
||||
title: None
|
||||
})
|
||||
);
|
||||
fn prep(content: &str) -> String {
|
||||
preprocess(content, OnFailure::Continue).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -341,28 +337,25 @@ A simple admonition.
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r#"# Chapter
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div class="admonition note">
|
||||
<div id="admonition-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<p>
|
||||
|
||||
Note
|
||||
|
||||
</p>
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"#;
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, preprocess(content).unwrap());
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -374,28 +367,25 @@ A simple admonition.
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r#"# Chapter
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div class="admonition warning">
|
||||
<div id="admonition-warning" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
<p>
|
||||
|
||||
Warning
|
||||
|
||||
</p>
|
||||
<a class="admonition-anchor-link" href="#admonition-warning"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"#;
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, preprocess(content).unwrap());
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -407,28 +397,25 @@ A simple admonition.
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r#"# Chapter
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div class="admonition warning">
|
||||
<div id="admonition-read-this" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
<p>
|
||||
|
||||
Read **this**!
|
||||
|
||||
</p>
|
||||
<a class="admonition-anchor-link" href="#admonition-read-this"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"#;
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, preprocess(content).unwrap());
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -448,7 +435,7 @@ Text
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, preprocess(content).unwrap());
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -468,7 +455,7 @@ Text
|
||||
</del>
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, preprocess(content).unwrap());
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -492,7 +479,7 @@ Text
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, preprocess(content).unwrap());
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -504,28 +491,25 @@ With <b>html</b> styling.
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
let expected = r##"
|
||||
|
||||
<div class="admonition note">
|
||||
<div id="admonition-and-in-the-title" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<p>
|
||||
|
||||
And "<i>in</i>" the title
|
||||
|
||||
</p>
|
||||
<a class="admonition-anchor-link" href="#admonition-and-in-the-title"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
|
||||
With <b>html</b> styling.
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
hello
|
||||
"#;
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, preprocess(content).unwrap());
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -537,27 +521,261 @@ Should be respected
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
let expected = r##"
|
||||
|
||||
<div class="admonition warning">
|
||||
<div id="admonition-trademark" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
<p>
|
||||
|
||||
Trademark™
|
||||
|
||||
</p>
|
||||
<a class="admonition-anchor-link" href="#admonition-trademark"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
|
||||
Should be respected
|
||||
|
||||
</p>
|
||||
</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
|
||||
```
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, preprocess(content).unwrap());
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
39
src/types.rs
Normal file
39
src/types.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum Directive {
|
||||
Note,
|
||||
Abstract,
|
||||
Info,
|
||||
Tip,
|
||||
Success,
|
||||
Question,
|
||||
Warning,
|
||||
Failure,
|
||||
Danger,
|
||||
Bug,
|
||||
Example,
|
||||
Quote,
|
||||
}
|
||||
|
||||
impl FromStr for Directive {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(string: &str) -> Result<Self, ()> {
|
||||
match string {
|
||||
"note" => Ok(Self::Note),
|
||||
"abstract" | "summary" | "tldr" => Ok(Self::Abstract),
|
||||
"info" | "todo" => Ok(Self::Info),
|
||||
"tip" | "hint" | "important" => Ok(Self::Tip),
|
||||
"success" | "check" | "done" => Ok(Self::Success),
|
||||
"question" | "help" | "faq" => Ok(Self::Question),
|
||||
"warning" | "caution" | "attention" => Ok(Self::Warning),
|
||||
"failure" | "fail" | "missing" => Ok(Self::Failure),
|
||||
"danger" | "error" => Ok(Self::Danger),
|
||||
"bug" => Ok(Self::Bug),
|
||||
"example" => Ok(Self::Example),
|
||||
"quote" | "cite" => Ok(Self::Quote),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user