Compare commits

..

51 Commits

Author SHA1 Message Date
Tom Milligan
92b441a950 bin: better error handling 2022-11-26 21:16:40 +00:00
dependabot[bot]
5530da074b chore(deps): bump JamesIves/github-pages-deploy-action
Bumps [JamesIves/github-pages-deploy-action](https://github.com/JamesIves/github-pages-deploy-action) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/JamesIves/github-pages-deploy-action/releases)
- [Commits](https://github.com/JamesIves/github-pages-deploy-action/compare/v4.4.0...v4.4.1)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 15:57:30 +00:00
Tom Milligan
0fe2ad52ed chore: cargo dep updates 2022-11-01 15:18:58 +00:00
Tom Milligan
787744f1f1 chore: prep v1.8.0 release 2022-10-23 15:20:31 +01:00
Tom Milligan
603c83e2eb chore: upgrade to clap v4 2022-10-16 09:32:06 +01:00
Tom Milligan
fbdffaa723 chore: upgrade outdated dep versions 2022-10-16 09:32:06 +01:00
dependabot[bot]
7c20d5b2d1 chore(deps): bump actions/cache from 2 to 3
Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-16 08:32:45 +01:00
dependabot[bot]
3507d6b5e0 chore(deps): bump JamesIves/github-pages-deploy-action
Bumps [JamesIves/github-pages-deploy-action](https://github.com/JamesIves/github-pages-deploy-action) from 4.3.3 to 4.4.0.
- [Release notes](https://github.com/JamesIves/github-pages-deploy-action/releases)
- [Commits](https://github.com/JamesIves/github-pages-deploy-action/compare/v4.3.3...v4.4.0)

---
updated-dependencies:
- dependency-name: JamesIves/github-pages-deploy-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-02 10:51:41 +01:00
dependabot[bot]
f81d4d40dd chore(deps): bump actions/download-artifact from 2 to 3
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-02 10:51:08 +01:00
dependabot[bot]
ed019a92d9 chore(deps): bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-02 10:51:00 +01:00
dependabot[bot]
9dd2ca128c chore(deps): bump actions/upload-artifact from 2 to 3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-02 10:50:51 +01:00
Matthias Beyer
51120acfd9 Add monthly dependabot
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
2022-10-02 10:08:38 +01:00
Tom Milligan
650123645b chore: upgrade dependencies 2022-10-02 09:22:46 +01:00
Tom Milligan
438c1dff5a ci: automatically deploy docs after publish 2022-06-11 10:23:30 +01:00
Tom Milligan
78b7451e49 chore: refactor scripts 2022-06-11 10:23:30 +01:00
Tom Milligan
bb937dc2d2 docs: update readme, documentation 2022-06-11 10:23:30 +01:00
Tom Milligan
db7101cb12 chore: prepare v1.7.0 release 2022-06-11 10:23:30 +01:00
Tom Milligan
fd6c2d0bd0 fix: try config v2 before v1 2022-06-11 10:23:30 +01:00
gggto
72deb8421c feat: add collapsible support 2022-05-18 13:24:00 +01:00
Tom Milligan
7b5a13d6af feat: make anchor links hoverable 2022-05-15 19:09:19 +01:00
Tom Milligan
28dfc5b6c3 feat: better bug output, option to fail mdbook build 2022-05-01 22:12:15 +01:00
Tom Milligan
beb640077f feat: add key/value configuration 2022-05-01 21:48:35 +01:00
Tom Milligan
6b479255a6 chore: prepare v1.6.0 release 2022-04-28 20:07:21 +01:00
Tom Milligan
7fe2fdc329 chore: polish error messages for breaking minor installs 2022-04-28 20:07:21 +01:00
Tom Milligan
e61e6148b5 fix: header bar overflow on firefox 2022-04-27 10:23:13 +01:00
Tom Milligan
0e235cb9b5 feat: clickable anchor links, enforce asset updates 2022-04-27 08:42:38 +01:00
Tom Milligan
c28f2b2fc9 chore: prepare v1.5.0 release 2022-04-25 21:31:54 +01:00
Tom Milligan
2a78ccbc2a ci: force install additional bins 2022-04-25 16:00:38 +01:00
Tom Milligan
5b61637eb7 feat: add unique ids to generated html 2022-04-25 16:00:38 +01:00
Tom Milligan
9786269b49 chore: prepare for v1.4.1 release 2022-04-21 19:33:11 +01:00
Tom Milligan
6517f50353 ci: add deployment workflow for binary artefacts 2022-04-21 19:33:11 +01:00
Tom Milligan
6dc759e358 chore: bump dependency lockfile 2022-04-21 19:33:11 +01:00
Tom Milligan
99f17dd2c5 chore: fix clippy lints 2022-02-28 22:04:20 +00:00
Tom Milligan
7c6f878d6c readme: update links to book 2022-02-28 22:04:20 +00:00
Tom Milligan
3fa5067be9 book: add example book with directive reference 2022-02-28 22:04:20 +00:00
Tom Milligan
8630161fa3 chore: prepare v1.4.0 release 2022-02-21 23:03:48 +00:00
Tom Milligan
0b50fd68ba feat: remove title bar if title empty 2022-02-21 19:39:03 +00:00
Tom Milligan
d851076cbc ci: speed up fast failures 2022-02-21 18:35:12 +00:00
Stephen Chung
af038017d2 feat: support for custom styles via directive.style 2022-02-21 18:11:38 +00:00
Tom Milligan
9fc872b8b8 ci: commit missed integration test file 2022-02-21 14:15:24 +00:00
Tom Milligan
b667271080 fix: remove superfluous <p> tags 2022-02-21 13:08:41 +00:00
Tom Milligan
0417d55ab8 ci: add mdbook integration test 2022-02-21 12:38:15 +00:00
Tom Milligan
82c5a3d5b9 chore: prepare v1.3.1 release 2022-02-21 11:50:36 +00:00
Tom Milligan
ada76bd4bd ci: add github actions, scripts 2022-02-21 11:20:47 +00:00
Tom Milligan
df79d57463 internal: factor out single function core 2022-02-20 20:13:43 +00:00
Tom Milligan
44097136f5 chore: prepare v1.3.2 release 2022-02-20 19:23:22 +00:00
Tom Milligan
455b4c586c readme: add note on JSON escapes 2022-02-20 19:17:40 +00:00
Tom Milligan
1915d10d7e fix: incorrect slicing of non-ascii info strings 2022-02-20 19:11:28 +00:00
Tom Milligan
583bc94542 chore: prepare v1.3.1 release 2022-02-19 10:38:02 +00:00
Tom Milligan
39746696ed fix: dedent generated HTML to avoid code block styling 2022-02-19 10:36:18 +00:00
Tom Milligan
dc9ba9eaa2 fix: update span splitting logic 2022-02-19 10:32:21 +00:00
40 changed files with 2950 additions and 989 deletions

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: cargo
directory: "/"
schedule:
interval: monthly
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: monthly

105
.github/workflows/check.yml vendored Normal file
View File

@@ -0,0 +1,105 @@
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@v3
- name: Cache build files
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
~/.cargo/bin
cargo_target
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Install additional test dependencies
env:
CARGO_TARGET_DIR: cargo_target
run: ./scripts/install
- name: Run check script
run: ./scripts/check
# Test on all supported platforms
test:
needs: fast-test
name: Test all other targets
strategy:
matrix:
os:
- ubuntu-20.04
# - windows-2019
rust:
- stable
- beta
- 1.60.0
experimental:
- false
# Run a canary test on nightly that's allowed to fail
include:
- os: ubuntu-20.04
rust: nightly
experimental: true
# Don't bother retesting stable linux, we did it in the comprehensive test
exclude:
- os: ubuntu-20.04
rust: stable
experimental: false
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Cache build files
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: test-${{ matrix.os }}-${{ matrix.rust }}-cargo-${{ hashFiles('**/Cargo.toml') }}
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- name: Run tests
run: cargo test

121
.github/workflows/deploy.yml vendored Normal file
View 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
View 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

31
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: publish
on:
release:
types: [published]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
~/.cargo/bin
cargo_target
# We reuse the cache from our detailed test environment, if available
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Publish crate
env:
CARGO_LOGIN_TOKEN: ${{ secrets.CARGO_LOGIN_TOKEN }}
run: ./scripts/publish

View File

@@ -1,5 +1,95 @@
## 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
- Fixed compilation failure with no default features
- MSRV (minimum supported rust version) documented as 1.58.0
## 1.3.2
### Fixed
- Fixed incorrect admonition title/panic when terminating with non-ascii characters
- Updated readme to note double-JSON string escapes
## 1.3.1
### Fixed
- Flattened indentation of generated HTML, otherwise it's styled as a markdown code block
- Fixed edge cases where the info string changes length when parsed, causing title/body to be incorrectly split
## 1.3.0
### Added

1242
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
[package]
name = "mdbook-admonish"
version = "1.3.0"
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,21 +22,27 @@ 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 = { version = "1.0.79", optional = true }
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"]
# Enable the command line binary
cli = ["clap", "env_logger", "log", "serde_json"]
cli = ["clap", "env_logger", "log"]
# Enable installation of files and configuration
cli-install = ["toml_edit"]

125
README.md
View File

@@ -1,10 +1,9 @@
# mdbook-admonish
[![Latest version](https://img.shields.io/crates/v/mdbook-admonish.svg)](https://crates.io/crates/mdbook-admonish)
[![docs.rs](https://img.shields.io/docsrs/mdbook-admonish)](https://docs.rs/mdbook-admonish)
[![docs.rs](https://img.shields.io/badge/docs-available-brightgreen)](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:
![Simple Message](img/simple-message.png)
## 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!
![Best Example](img/best-example.png)
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,18 +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:
````
```admonish warning "Data loss"
The following steps can lead to irrecoverable data corruption.
```
````
![Data Loss](img/data-loss.png)
You can also leave out the admonition type altogether, in which case it will default to `note`:
````
@@ -61,30 +56,13 @@ A plain note.
![Plain Note](img/plain-note.png)
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:
![Complex Message](img/complex-message.png)
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!"
```
~~~
````
![Code Bug](img/code-bug.png)
- Custom titles
- Custom styling
- Collapsible blocks
## Installation
@@ -97,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
@@ -123,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
@@ -135,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
View File

@@ -0,0 +1,2 @@
/book
/mdbook-admonish.css

17
book/book.toml Normal file
View 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
View File

@@ -0,0 +1,4 @@
# Summary
- [Overview](./overview.md)
- [Reference](./reference.md)

166
book/src/overview.md Normal file
View File

@@ -0,0 +1,166 @@
# mdbook-admonish
[![Latest version](https://img.shields.io/crates/v/mdbook-admonish.svg)](https://crates.io/crates/mdbook-admonish)
[![docs.rs](https://img.shields.io/docsrs/mdbook-admonish)](https://docs.rs/mdbook-admonish)
A preprocessor for [mdbook](https://github.com/rust-lang-nursery/mdBook) to add [Material Design](https://material.io/design) admonishments, based on the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) implementation.
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
View 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.
```

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

2
integration/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/book
/mdbook-admonish.css

17
integration/book.toml Normal file
View 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"]

View 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"]

View 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=&quot;
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
View 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

View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"/..
sed '1,/<main>/d;/<\/main>/,$d' book/chapter_1.html

View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"/..
./scripts/get-snapshot > expected/chapter_1_main.html

View File

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

View 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
View 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"

40
scripts/check Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"/..
function eprintln() {
>&2 echo "$1"
}
eprintln "Formatting sources"
cargo fmt -- --check
# Known issues:
# - RUSTSEC-2020-0071 known unlikely segfault in `time`
# - RUSTSEC-2020-0016 `net2` is unmaintained
# - RUSTSEC-2020-0159 known unlikely segfault in `chrono`
# - RUSTSEC-2021-0145 known unmaintained atty transitive dep
eprintln "Auditing dependencies"
cargo audit --deny warnings \
--ignore RUSTSEC-2020-0071 \
--ignore RUSTSEC-2020-0016 \
--ignore RUSTSEC-2021-0145 \
--ignore RUSTSEC-2020-0159
eprintln "Linting sources"
cargo clippy --all-targets -- -D warnings
eprintln "Running tests (default)"
cargo test
eprintln "Running tests (no features)"
cargo test --no-default-features
eprintln "Running tests (cli)"
cargo test --no-default-features --features cli
eprintln "Building documentation"
cargo doc --no-deps --lib
eprintln "Running mdbook integration test"
./integration/scripts/check

17
scripts/install Executable file
View File

@@ -0,0 +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
if ! cargo audit --version; then
cargo install cargo-audit --force
fi
./scripts/install-mdbook

9
scripts/install-mdbook Executable file
View File

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

13
scripts/publish Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
set -euxo pipefail
cd "$(dirname "$0")"/..
# Don't log the cargo login token while authenticating
set +x
echo "cargo login ***********************************"
cargo login "${CARGO_LOGIN_TOKEN}"
set -x
cargo publish

18
scripts/rebuild-book Executable file
View 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

View File

@@ -0,0 +1 @@
^2.0.0

1
src/bin/assets/VERSION Normal file
View File

@@ -0,0 +1 @@
2.0.0

View File

@@ -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);
}

View File

@@ -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,24 +181,22 @@ 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.
///
/// Return `Err` if the existing configuration is unknown.
fn additional_css<'a>(doc: &'a mut Document) -> Result<&'a mut Array, ()> {
fn additional_css(doc: &mut Document) -> Result<&mut Array, ()> {
let doc = doc.as_table_mut();
let empty_table = Item::Table(Table::default());
let empty_array = Item::Value(Value::Array(Array::default()));
Ok(doc
.entry("output")
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()?
@@ -197,8 +205,7 @@ A beautifully styled message.
.as_value_mut()?
.as_array_mut()
})
.flatten()
.ok_or(())?)
.ok_or(())
}
/// Return the preprocessor table for admonish, initializing if required.

158
src/config/mod.rs Normal file
View 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
View 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
View 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()
);
}
}

View File

@@ -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::add_admonish(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,82 +137,153 @@ impl Directive {
}
}
#[derive(Debug, PartialEq)]
struct AdmonitionInfo<'a> {
directive: &'a str,
title: Option<&'a str>,
}
#[derive(Debug, PartialEq)]
struct Admonition<'a> {
directive: Directive,
title: Cow<'a, str>,
title: Option<String>,
content: Cow<'a, str>,
additional_classnames: Vec<String>,
collapsible: bool,
}
impl<'a> Default for Admonition<'a> {
fn default() -> Self {
Self {
directive: Directive::Note,
title: Cow::Borrowed("Note"),
}
}
}
impl<'a> TryFrom<AdmonitionInfo<'a>> for Admonition<'a> {
type Error = ();
fn try_from(other: AdmonitionInfo<'a>) -> Result<Self, ()> {
let directive = Directive::from_str(other.directive)?;
Ok(Self {
impl<'a> Admonition<'a> {
pub fn new(info: AdmonitionInfo, content: &'a str) -> Self {
let AdmonitionInfo {
directive,
title: other
.title
.map(Cow::Borrowed)
.unwrap_or_else(|| Cow::Owned(ucfirst(other.directive))),
})
title,
additional_classnames,
collapsible,
} = info;
Self {
directive,
title,
content: Cow::Borrowed(content),
additional_classnames,
collapsible,
}
}
fn html(&self, anchor_id: &str) -> String {
let mut additional_class = Cow::Borrowed(self.directive.classname());
let title = &self.title;
let content = &self.content;
let title_block = if self.collapsible { "summary" } else { "div" };
let title_html = title
.as_ref()
.map(|title| {
Cow::Owned(format!(
r##"<{title_block} class="admonition-title">
{title}
<a class="admonition-anchor-link" href="#{ANCHOR_ID_PREFIX}-{anchor_id}"></a>
</{title_block}>
"##
))
})
.unwrap_or(Cow::Borrowed(""));
if !self.additional_classnames.is_empty() {
let mut buffer = additional_class.into_owned();
for additional_classname in &self.additional_classnames {
buffer.push(' ');
buffer.push_str(additional_classname);
}
additional_class = Cow::Owned(buffer);
}
let admonition_block = if self.collapsible { "details" } else { "div" };
// Notes on the HTML template:
// - the additional whitespace around the content are deliberate
// In line with the commonmark spec, this allows the inner content to be
// rendered as markdown paragraphs.
format!(
r#"<{admonition_block} id="{ANCHOR_ID_PREFIX}-{anchor_id}" class="admonition {additional_class}">
{title_html}<div>
{content}
</div>
</{admonition_block}>"#,
)
}
}
/// Returns:
/// - `None` if this is not an `admonish` block.
/// - `Some(None)` if this is an `admonish` block, but no further configuration was given
/// - `Some(AdmonitionInfo)` if this is an `admonish` block, and further configuration was given
fn parse_info_string(info_string: &str) -> Option<Option<AdmonitionInfo>> {
if info_string == "admonish" {
return Some(None);
}
const ANCHOR_ID_PREFIX: &str = "admonition";
const ANCHOR_ID_DEFAULT: &str = "default";
let directive_title = match info_string.split_once(' ') {
Some(("admonish", rest)) => rest,
_ => return None,
};
fn extract_admonish_body(content: &str) -> &str {
const PRE_END: char = '\n';
const POST: &str = "```";
let info = if let Some((directive, title)) = directive_title.split_once(' ') {
// The title is expected to be a quoted JSON string
let title = serde_json::from_str(title).ok();
AdmonitionInfo { directive, title }
} else {
AdmonitionInfo {
directive: directive_title,
title: None,
// We can't trust the info string length to find the start of the body
// it may change length if it contains HTML or character escapes.
//
// So we scan for the first newline and use that.
// If gods forbid it doesn't exist for some reason, just include the whole info string.
let start_index = content
// Start one character _after_ the newline
.find(PRE_END)
.map(|index| index + 1)
.unwrap_or_default();
let end_index = content.len() - POST.len();
let admonish_content = &content[start_index..end_index];
// The newline after a code block is technically optional, so we have to
// trim it off dynamically.
admonish_content.trim()
}
/// Given the content in the span of the code block, and the info string,
/// return `Some(Admonition)` if the code block is an admonition.
///
/// If there is an error parsing the admonition, either:
///
/// - Display a UI error message output in the book.
/// - If configured, break the build.
///
/// If the code block is not an admonition, return `None`.
fn parse_admonition<'a>(
info_string: &'a str,
content: &'a str,
on_failure: OnFailure,
) -> Option<MdbookResult<Admonition<'a>>> {
let info = AdmonitionInfo::from_info_string(info_string)?;
let info = match info {
Ok(info) => info,
// FIXME return error messages to break build if configured
// Err(message) => return Some(Err(content)),
Err(message) => {
return Some(match on_failure {
OnFailure::Continue => Ok(Admonition {
directive: Directive::Bug,
title: Some("Error rendering admonishment".to_owned()),
additional_classnames: Vec::new(),
collapsible: false,
content: Cow::Owned(format!(
r#"Failed with: {message}
Original markdown input:
``````
{content}
``````
"#
)),
}),
OnFailure::Bail => Err(anyhow!("Error processing admonition, bailing:\n{content}")),
})
}
};
Some(Some(info))
let body = extract_admonish_body(content);
Some(Ok(Admonition::new(info, body)))
}
/// 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(),
}
}
fn add_admonish(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);
@@ -178,54 +295,18 @@ fn add_admonish(content: &str) -> MdbookResult<String> {
let events = Parser::new_ext(content, opts);
for (e, span) in events.into_offset_iter() {
if let Event::Start(Tag::CodeBlock(Fenced(info_string))) = e.clone() {
let info = match parse_info_string(info_string.as_ref()) {
Some(info) => info,
let span_content = &content[span.start..span.end];
let admonition = match parse_admonition(info_string.as_ref(), span_content, on_failure)
{
Some(admonition) => admonition,
None => continue,
};
let admonition = info
.map(|info| Admonition::try_from(info).unwrap_or_default())
.unwrap_or_default();
const PRE_START: &str = "```";
const PRE_END: &str = "\n";
const POST: &str = "```";
let start_index = span.start + PRE_START.len() + info_string.len() + PRE_END.len();
let end_index = span.end - POST.len();
let admonish_content = &content[start_index..end_index];
let admonish_content = admonish_content.trim();
// 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>.
let admonish_code = format!(
r#"<div class="admonition {directive_classname}">
<div class="admonition-title">
<p>
{directive_title}
</p>
</div>
<div>
<p>
{admonish_content}
</p>
</div>
</div>"#,
directive_classname = admonition.directive.classname(),
directive_title = admonition.title,
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, admonish_code.clone()));
admonish_blocks.push((span, admonition.html(&anchor_id)));
}
}
@@ -238,44 +319,13 @@ fn add_admonish(content: &str) -> MdbookResult<String> {
Ok(content)
}
impl Admonish {
fn add_admonish(chapter: &mut Chapter) -> MdbookResult<String> {
add_admonish(&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(None));
assert_eq!(
parse_info_string("admonish "),
Some(Some(AdmonitionInfo {
directive: "",
title: None,
}))
);
assert_eq!(
parse_info_string("admonish unknown"),
Some(Some(AdmonitionInfo {
directive: "unknown",
title: None
}))
);
assert_eq!(
parse_info_string("admonish note"),
Some(Some(AdmonitionInfo {
directive: "note",
title: None
}))
);
fn prep(content: &str) -> String {
preprocess(content, OnFailure::Continue).unwrap()
}
#[test]
@@ -287,28 +337,25 @@ A simple admonition.
Text
"#;
let expected = r#"# Chapter
let expected = r##"# Chapter
<div class="admonition note">
<div class="admonition-title">
<p>
<div id="admonition-note" class="admonition note">
<div class="admonition-title">
Note
Note
</p>
</div>
<div>
<p>
<a class="admonition-anchor-link" href="#admonition-note"></a>
</div>
<div>
A simple admonition.
A simple admonition.
</p>
</div>
</div>
</div>
Text
"#;
"##;
assert_eq!(expected, add_admonish(content).unwrap());
assert_eq!(expected, prep(content));
}
#[test]
@@ -320,28 +367,25 @@ A simple admonition.
Text
"#;
let expected = r#"# Chapter
let expected = r##"# Chapter
<div class="admonition warning">
<div class="admonition-title">
<p>
<div id="admonition-warning" class="admonition warning">
<div class="admonition-title">
Warning
Warning
</p>
</div>
<div>
<p>
<a class="admonition-anchor-link" href="#admonition-warning"></a>
</div>
<div>
A simple admonition.
A simple admonition.
</p>
</div>
</div>
</div>
Text
"#;
"##;
assert_eq!(expected, add_admonish(content).unwrap());
assert_eq!(expected, prep(content));
}
#[test]
@@ -353,28 +397,25 @@ A simple admonition.
Text
"#;
let expected = r#"# Chapter
let expected = r##"# Chapter
<div class="admonition warning">
<div class="admonition-title">
<p>
<div id="admonition-read-this" class="admonition warning">
<div class="admonition-title">
Read **this**!
Read **this**!
</p>
</div>
<div>
<p>
<a class="admonition-anchor-link" href="#admonition-read-this"></a>
</div>
<div>
A simple admonition.
A simple admonition.
</p>
</div>
</div>
</div>
Text
"#;
"##;
assert_eq!(expected, add_admonish(content).unwrap());
assert_eq!(expected, prep(content));
}
#[test]
@@ -394,7 +435,7 @@ Text
| Row 1 | Row 2 |
"#;
assert_eq!(expected, add_admonish(content).unwrap());
assert_eq!(expected, prep(content));
}
#[test]
@@ -414,7 +455,7 @@ Text
</del>
"#;
assert_eq!(expected, add_admonish(content).unwrap());
assert_eq!(expected, prep(content));
}
#[test]
@@ -438,39 +479,303 @@ Text
2. paragraph 2
"#;
assert_eq!(expected, add_admonish(content).unwrap());
assert_eq!(expected, prep(content));
}
#[test]
fn html_in_admonish_untouched() {
fn info_string_that_changes_length_when_parsed() {
let content = r#"
```admonish note "And <i>in</i> the title"
```admonish note "And \\"<i>in</i>\\" the title"
With <b>html</b> styling.
```
hello
"#;
let expected = r#"
let expected = r##"
<div class="admonition note">
<div class="admonition-title">
<p>
<div id="admonition-and-in-the-title" class="admonition note">
<div class="admonition-title">
And <i>in</i> the title
And "<i>in</i>" the title
</p>
</div>
<div>
<p>
With <b>html</b> styling.
</p>
</div>
<a class="admonition-anchor-link" href="#admonition-and-in-the-title"></a>
</div>
<div>
With <b>html</b> styling.
</div>
</div>
hello
"##;
assert_eq!(expected, prep(content));
}
#[test]
fn info_string_ending_in_symbol() {
let content = r#"
```admonish warning "Trademark™"
Should be respected
```
hello
"#;
assert_eq!(expected, add_admonish(content).unwrap());
let expected = r##"
<div id="admonition-trademark" class="admonition warning">
<div class="admonition-title">
Trademark™
<a class="admonition-anchor-link" href="#admonition-trademark"></a>
</div>
<div>
Should be respected
</div>
</div>
hello
"##;
assert_eq!(expected, prep(content));
}
#[test]
fn block_with_additional_classname() {
let content = r#"
```admonish tip.my-style.other-style
Will have bonus classnames
```
"#;
let expected = r##"
<div id="admonition-tip" class="admonition tip my-style other-style">
<div class="admonition-title">
Tip
<a class="admonition-anchor-link" href="#admonition-tip"></a>
</div>
<div>
Will have bonus classnames
</div>
</div>
"##;
assert_eq!(expected, prep(content));
}
#[test]
fn block_with_additional_classname_and_title() {
let content = r#"
```admonish tip.my-style.other-style "Developers don't want you to know this one weird tip!"
Will have bonus classnames
```
"#;
let expected = r##"
<div id="admonition-developers-dont-want-you-to-know-this-one-weird-tip" class="admonition tip my-style other-style">
<div class="admonition-title">
Developers don't want you to know this one weird tip!
<a class="admonition-anchor-link" href="#admonition-developers-dont-want-you-to-know-this-one-weird-tip"></a>
</div>
<div>
Will have bonus classnames
</div>
</div>
"##;
assert_eq!(expected, prep(content));
}
#[test]
fn block_with_empty_additional_classnames_title_content() {
let content = r#"
```admonish .... ""
```
"#;
let expected = r##"
<div id="admonition-default" class="admonition note">
<div>
</div>
</div>
"##;
assert_eq!(expected, prep(content));
}
#[test]
fn unique_ids_same_title() {
let content = r#"
```admonish note "My Note"
Content zero.
```
```admonish note "My Note"
Content one.
```
"#;
let expected = r##"
<div id="admonition-my-note" class="admonition note">
<div class="admonition-title">
My Note
<a class="admonition-anchor-link" href="#admonition-my-note"></a>
</div>
<div>
Content zero.
</div>
</div>
<div id="admonition-my-note-1" class="admonition note">
<div class="admonition-title">
My Note
<a class="admonition-anchor-link" href="#admonition-my-note-1"></a>
</div>
<div>
Content one.
</div>
</div>
"##;
assert_eq!(expected, prep(content));
}
#[test]
fn v2_config_works() {
let content = r#"
```admonish tip class="my other-style" title="Article Heading"
Bonus content!
```
"#;
let expected = r##"
<div id="admonition-article-heading" class="admonition tip my other-style">
<div class="admonition-title">
Article Heading
<a class="admonition-anchor-link" href="#admonition-article-heading"></a>
</div>
<div>
Bonus content!
</div>
</div>
"##;
assert_eq!(expected, prep(content));
}
#[test]
fn continue_on_error_output() {
let content = r#"
```admonish title="
Bonus content!
```
"#;
let expected = r##"
<div id="admonition-error-rendering-admonishment" class="admonition bug">
<div class="admonition-title">
Error rendering admonishment
<a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a>
</div>
<div>
Failed with: TOML parsing error: unterminated string at line 1 column 7
Original markdown input:
``````
```admonish title="
Bonus content!
```
``````
</div>
</div>
"##;
assert_eq!(expected, prep(content));
}
#[test]
fn bail_on_error_output() {
let content = r#"
```admonish title="
Bonus content!
```
"#;
assert_eq!(
preprocess(content, OnFailure::Bail)
.unwrap_err()
.to_string(),
r#"Error processing admonition, bailing:
```admonish title="
Bonus content!
```"#
.to_owned()
)
}
#[test]
fn block_collapsible() {
let content = r#"
```admonish collapsible=true
Hidden
```
"#;
let expected = r##"
<details id="admonition-note" class="admonition note">
<summary class="admonition-title">
Note
<a class="admonition-anchor-link" href="#admonition-note"></a>
</summary>
<div>
Hidden
</div>
</details>
"##;
assert_eq!(expected, prep(content));
}
}

39
src/types.rs Normal file
View 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(()),
}
}
}