Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b99630878f | ||
|
|
7e774f4655 | ||
|
|
823cefbcbc | ||
|
|
a6a2941821 | ||
|
|
faf99a1b76 | ||
|
|
afdc2b03d0 | ||
|
|
e55df3e60b | ||
|
|
f3d49b93de | ||
|
|
92caf95b34 | ||
|
|
0742c6c1e8 | ||
|
|
60706be3e0 | ||
|
|
f5a6b9ef0f | ||
|
|
9361b7e7fa | ||
|
|
24bef47b15 | ||
|
|
0eb5fd35c3 | ||
|
|
de539cd0fd | ||
|
|
4842daea1c | ||
|
|
76212fccfb | ||
|
|
681c991a9a | ||
|
|
1e0a3992d5 | ||
|
|
d97747d195 | ||
|
|
2e2cebfc83 | ||
|
|
ab595f18f7 | ||
|
|
196585f4f2 | ||
|
|
7ad4d3f18c | ||
|
|
0324c93efa | ||
|
|
62dd36624d | ||
|
|
b3e82df34e | ||
|
|
e8813eb104 | ||
|
|
f606ad8758 | ||
|
|
d269838765 | ||
|
|
082359e562 | ||
|
|
84d163c32f | ||
|
|
90484a44ea | ||
|
|
5e7674d1f9 | ||
|
|
55a654dfca | ||
|
|
a2f664cac3 | ||
|
|
dfb70b0415 | ||
|
|
fb1b789386 | ||
|
|
97bcd97c64 | ||
|
|
a2a7316b26 | ||
|
|
f9eb198cd0 | ||
|
|
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 | ||
|
|
82c5a3d5b9 | ||
|
|
ada76bd4bd | ||
|
|
df79d57463 | ||
|
|
44097136f5 | ||
|
|
455b4c586c | ||
|
|
1915d10d7e | ||
|
|
583bc94542 | ||
|
|
39746696ed | ||
|
|
dc9ba9eaa2 | ||
|
|
34e120e414 | ||
|
|
3074784c42 | ||
|
|
33009801da | ||
|
|
ed029aea5d | ||
|
|
5177b76d76 | ||
|
|
ab73e41359 | ||
|
|
6127679df8 | ||
|
|
9367948599 |
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
|
||||
105
.github/workflows/check.yml
vendored
Normal 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.66.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
|
||||
153
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
# 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 | Extract version
|
||||
shell: bash
|
||||
run: echo "##[set-output name=version;]$(echo ${GITHUB_REF#refs/tags/v})"
|
||||
id: extract_version
|
||||
|
||||
- name: Setup | Release notes
|
||||
run: |
|
||||
cat CHANGELOG.md | sed -n '/^## ${{ steps.extract_version.outputs.version }}$/,/^## /p' | sed '$d' > RELEASE.md
|
||||
- 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 }}
|
||||
|
||||
# Upload to crates.io
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
needs: github_release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
~/.cargo/bin
|
||||
cargo_target
|
||||
# We reuse the cache from our detailed test environment, if available
|
||||
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Publish crate
|
||||
env:
|
||||
CARGO_LOGIN_TOKEN: ${{ secrets.CARGO_LOGIN_TOKEN }}
|
||||
run: ./scripts/publish
|
||||
42
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
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: Install mdbook extras
|
||||
run: ./book/scripts/install-mdbook-extras
|
||||
- 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
|
||||
155
CHANGELOG.md
@@ -1,4 +1,157 @@
|
||||
## Changelog
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.10.2
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `cargo install mdbook-admonish` failing due to an internal dependency mismatch with `mdbook` ([#115](https://github.com/tommilligan/mdbook-admonish/pull/115))
|
||||
|
||||
## 1.10.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Only restyle `summary` elements generated by `mdbook-admonish`. Thanks to [@ImUrX](https://github.com/ImUrX) for the report and fix! ([#112](https://github.com/tommilligan/mdbook-admonish/pull/112))
|
||||
|
||||
## 1.10.0
|
||||
|
||||
### Changed
|
||||
|
||||
- MSRV (minimum supported rust version) is now 1.66.0 for mdbook v0.4.32 ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
|
||||
|
||||
### Added
|
||||
|
||||
- Support `mdbook test` running doctests inside admonish blocks. Opt-in to this by setting `renderer.test.action_mode = "strip"` ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
|
||||
- Log a warning when an invalid admonish block is encountered ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Document all `book.toml` configuration options [in the reference](https://tommilligan.github.io/mdbook-admonish/reference.html), some of which were previously undocumened ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
|
||||
|
||||
## 1.9.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Styles updated to `^2.0.1`. Run `mdbook-admonish install` to update.
|
||||
- MSRV (minimum supported rust version) is now 1.64.0 for clap v4 ([#79](https://github.com/tommilligan/mdbook-admonish/pull/79))
|
||||
- More verbose error messages for invalid TOML configurations ([#79](https://github.com/tommilligan/mdbook-admonish/pull/79))
|
||||
|
||||
### Added
|
||||
|
||||
- User can set book-wide default for title and collapsible properties ([#84](https://github.com/tommilligan/mdbook-admonish/pull/84)), thanks to [@ShaunSHamilton](https://github.com/ShaunSHamilton)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Custom installation and CSS directories are now normalized ([#49](https://github.com/tommilligan/mdbook-admonish/pull/49))
|
||||
- Fix title bars with no text rendering badly ([#83](https://github.com/tommilligan/mdbook-admonish/pull/83)), thanks to [@ShaunSHamilton](https://github.com/ShaunSHamilton)
|
||||
- Better error message display on crash ([#48](https://github.com/tommilligan/mdbook-admonish/pull/48))
|
||||
- Better support for commonmark code fence syntax ([#88](https://github.com/tommilligan/mdbook-admonish/pull/88), [#89](https://github.com/tommilligan/mdbook-admonish/pull/89))
|
||||
|
||||
## 1.8.0
|
||||
|
||||
### Changed
|
||||
|
||||
- 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 collapsible admonition bodies ([#26](https://github.com/tommilligan/mdbook-admonish/pull/26), thanks [@gggto](https://github.com/gggto) for the suggestion and implementation!)
|
||||
- Make anchor links hoverable ([#27](https://github.com/tommilligan/mdbook-admonish/pull/27))
|
||||
- Better handling for misconfigured admonitions ([#25](https://github.com/tommilligan/mdbook-admonish/pull/25))
|
||||
- Nicer in-book error messages
|
||||
- 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
|
||||
|
||||
- Add additional examples and images in readme
|
||||
- Allow markdown styling in title content
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix HTML being stripping from body content
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Added
|
||||
|
||||
- Support custom title text
|
||||
|
||||
## 1.1.0
|
||||
|
||||
|
||||
1892
Cargo.lock
generated
31
Cargo.toml
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "mdbook-admonish"
|
||||
version = "1.1.0"
|
||||
version = "1.10.2"
|
||||
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,30 @@ 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.72"
|
||||
clap = { version = "4", default_features = false, features = ["std", "derive"], optional = true }
|
||||
env_logger = { version = "0.10", default_features = false, optional = true }
|
||||
log = "0.4.19"
|
||||
mdbook = "0.4.34"
|
||||
once_cell = "1.18.0"
|
||||
pulldown-cmark = "0.9.3"
|
||||
regex = "1.9.3"
|
||||
semver = "1.0.18"
|
||||
serde = { version = "1.0.183", features = ["derive"] }
|
||||
serde_json = "1.0.104"
|
||||
# The version of toml that mdbook uses internally (and uses in it's public api)
|
||||
# Only used for compatilibilty with the mdbook public api
|
||||
toml_mdbook = { package = "toml", version = "0.5.11" }
|
||||
toml = "0.7.6"
|
||||
toml_edit = { version = "0.19.14", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.1.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
|
||||
[features]
|
||||
default = ["cli", "cli-install"]
|
||||
|
||||
# Enable the command line binary
|
||||
cli = ["clap", "env_logger", "log", "serde_json"]
|
||||
cli = ["clap", "env_logger"]
|
||||
# Enable installation of files and configuration
|
||||
cli-install = ["toml_edit"]
|
||||
|
||||
110
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:
|
||||
|
||||
@@ -16,7 +15,15 @@ A beautifully styled message.
|
||||
|
||||
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
|
||||
|
||||
@@ -28,15 +35,9 @@ My example is the best!
|
||||
```
|
||||
````
|
||||
|
||||
You can also leave out the admonition type, in which case it will default to `note`:
|
||||

|
||||
|
||||
````
|
||||
```admonish
|
||||
A plain note.
|
||||
```
|
||||
````
|
||||
|
||||
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`
|
||||
@@ -45,6 +46,24 @@ See the [mkdocs-material docs](https://squidfunk.github.io/mkdocs-material/refer
|
||||
|
||||
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.
|
||||
```
|
||||
````
|
||||
|
||||

|
||||
|
||||
### Additional Options
|
||||
|
||||
See the [`mdbook-admonish` book](https://tommilligan.github.io/mdbook-admonish/) for additional options, such as:
|
||||
|
||||
- Custom titles
|
||||
- Custom styling
|
||||
- Collapsible blocks
|
||||
|
||||
## Installation
|
||||
|
||||
Install the tool:
|
||||
@@ -56,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
|
||||
@@ -80,6 +101,52 @@ Then, build your book as usual:
|
||||
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.
|
||||
|
||||
`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
|
||||
```
|
||||
|
||||
### Process included files
|
||||
|
||||
You can ensure that content inlined with `{{#include}}` is also processed by [setting the `after` option](https://rust-lang.github.io/mdBook/format/configuration/preprocessors.html#require-a-certain-order):
|
||||
|
||||
```toml
|
||||
[preprocessor.admonish]
|
||||
after = ["links"]
|
||||
```
|
||||
|
||||
This will expand `include` directives, before expanding `admonish` blocks.
|
||||
|
||||
### 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
|
||||
|
||||
Project design
|
||||
@@ -88,6 +155,25 @@ 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.
|
||||
|
||||
## Thanks
|
||||
|
||||
This utility is heavily drawn from and inspired by other projects, namely:
|
||||
|
||||
2
book/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/book
|
||||
/mdbook-admonish.css
|
||||
21
book/book.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[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.2" # do not edit: managed by `mdbook-admonish install`
|
||||
|
||||
[preprocessor.toc]
|
||||
command = "mdbook-toc"
|
||||
renderer = ["html"]
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["./mdbook-admonish.css"]
|
||||
9
book/scripts/install-mdbook-extras
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -exuo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/../..
|
||||
|
||||
if ! mdbook-toc --version; then
|
||||
cargo install mdbook-toc --version 0.13.0 --force
|
||||
fi
|
||||
4
book/src/SUMMARY.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Summary
|
||||
|
||||
- [Overview](./overview.md)
|
||||
- [Reference](./reference.md)
|
||||
178
book/src/overview.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# mdbook-admonish
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Intoduction
|
||||
|
||||
[](https://crates.io/crates/mdbook-admonish)
|
||||
[](https://docs.rs/mdbook-admonish)
|
||||
|
||||
A preprocessor for [mdbook](https://github.com/rust-lang/mdBook) to add [Material Design](https://material.io/design) admonishments, based on the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) implementation.
|
||||
|
||||
It turns this:
|
||||
|
||||
````
|
||||
```admonish info
|
||||
A beautifully styled message.
|
||||
```
|
||||
````
|
||||
|
||||
into this:
|
||||
|
||||
```admonish info
|
||||
A beautifully styled message.
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### A basic `admonish` block
|
||||
|
||||
Use any [fenced code-block](https://spec.commonmark.org/0.30/#fenced-code-blocks) as you normally would, but annotate it with `admonish <admonition type>`:
|
||||
|
||||
````
|
||||
```admonish example
|
||||
My example is the best!
|
||||
```
|
||||
````
|
||||
|
||||
```admonish example
|
||||
My example is the best!
|
||||
```
|
||||
|
||||
See the [list of directives](./reference.md#directives) for a full 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.
|
||||
```
|
||||
|
||||
### Invalid blocks
|
||||
|
||||
By default, if an `admonish` block cannot be parsed, an error will be rendered in the output:
|
||||
|
||||
````
|
||||
```admonish title="\j"
|
||||
This block will error
|
||||
```
|
||||
````
|
||||
|
||||
```admonish title="\j"
|
||||
This block will error
|
||||
```
|
||||
|
||||
You can also configure the build to fail loudly, by setting `on_failure = "bail"` in `book.toml`. See the [configuration reference](./reference.md#booktoml-configuration) for more details.
|
||||
|
||||
### Additional Options
|
||||
|
||||
You can pass additional options to each block. The options are structured as TOML key-value pairs.
|
||||
|
||||
Note that some options can be passed globally, through the `default` section in `book.toml`. See the [configuration reference](./reference.md#booktoml-configuration) for more details.
|
||||
|
||||
#### Custom title
|
||||
|
||||
A custom title can be provided, contained in a double quoted TOML string.
|
||||
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.
|
||||
```
|
||||
165
book/src/reference.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# Reference
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## `book.toml` configuration
|
||||
|
||||
See below for all configuration options available to add in `book.toml`.
|
||||
|
||||
The options should all be nested under `preprocessor.admonish`; for example:
|
||||
|
||||
```toml
|
||||
[preprocessor.admonish]
|
||||
on_failure = "bail"
|
||||
|
||||
[preprocessor.admonish.default]
|
||||
collapsible = true
|
||||
|
||||
[preprocessor.admonish.renderer.test]
|
||||
render_mode = "strip"
|
||||
```
|
||||
|
||||
### `on_failure`
|
||||
|
||||
Optional. Default value: `continue`.
|
||||
|
||||
The action to take when an invalid `admonish` block is encountered:
|
||||
|
||||
- `continue` (default): Continue processing future blocks, do not fail the build. If rendering to HTML, an error message will be displayed in the book output.
|
||||
- `bail`: Abort the build.
|
||||
|
||||
### `default`
|
||||
|
||||
Optional.
|
||||
|
||||
Default values to use, when not provided in an `admonish` block explicitly.
|
||||
|
||||
Subfields:
|
||||
|
||||
- `default.title` (optional): Title to use for blocks. Defaults to the directive used in titlecase.
|
||||
- `default.collapsible` (optional, default: `false`): Make blocks collapsible by default when set to `true`.
|
||||
|
||||
### `renderer`
|
||||
|
||||
````admonish tip
|
||||
It is recommended that you set:
|
||||
|
||||
```toml
|
||||
[preprocessor.admonish.renderer.test]
|
||||
render_mode = "strip"
|
||||
```
|
||||
|
||||
This allows `mdbook test` to find and test rust examples within `admonish` blocks.
|
||||
|
||||
This will be the default behaviour in the next `mdbook-admonish` major version.
|
||||
````
|
||||
|
||||
Optional.
|
||||
|
||||
Additional settings to apply, depending on the renderer that is running.
|
||||
|
||||
The most common renderers used are:
|
||||
|
||||
- `html`: Used by `mdbook build` to build the final book output.
|
||||
- `test`: Used by `mdbook test` to find and run doctests.
|
||||
|
||||
Subfields:
|
||||
|
||||
- `renderer.<renderer_name>.render_mode` (optional): The action `mdbook-admonish` should take when running with this renderer.
|
||||
- Valid values:
|
||||
- `html`: Convert `admonish` blocks into HTML output.
|
||||
- `preserve`: Do nothing. Leave the book untouched.
|
||||
- `strip`: Strip `admonish`-specific syntax, leaving the inner content untouched.
|
||||
- Default values:
|
||||
- For the `html` renderer, the default value is `html`.
|
||||
- For all other renderers, the default value is `preserve`.
|
||||
|
||||
### `command`
|
||||
|
||||
Required.
|
||||
|
||||
Used by `mdbook` to know how to call the `mdbook-admonish` plugin.
|
||||
|
||||
Running this command with the `--version` flag from your shell should work, for the plugin to function.
|
||||
|
||||
### `assets_version`
|
||||
|
||||
Optional.
|
||||
|
||||
This is automatically updated by `mdbook-admonish install` and should not be edited.
|
||||
|
||||
## Directives
|
||||
|
||||
All supported directives are listed below.
|
||||
|
||||
`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.
|
||||
```
|
||||
6
book/v2.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Notes for a v2 release
|
||||
|
||||
## Default behaviour changes
|
||||
|
||||
- `on_failure` default changed from `continue` to `bail`
|
||||
- `preprocessor.admonish.renderer.test.render_mode` default changed from `preserve` to `strip`
|
||||
@@ -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,18 +116,49 @@ $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) {
|
||||
:is(.admonition-title, summary.admonition-title) {
|
||||
position: relative;
|
||||
min-height: 4rem;
|
||||
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 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Adjust spacing for title-only admonitions
|
||||
html &:last-child {
|
||||
@@ -136,7 +169,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;
|
||||
@@ -148,6 +181,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);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -170,9 +233,8 @@ $admonitions: (
|
||||
}
|
||||
|
||||
// Admonition flavour title
|
||||
:is(#{$flavours}) > :is(.admonition-title, summary) {
|
||||
:is(#{$flavours}) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: color.adjust($tint, $alpha: -0.9);
|
||||
border-color: $tint;
|
||||
|
||||
// Admonition icon
|
||||
&::before {
|
||||
@@ -210,4 +272,9 @@ $admonitions: (
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
& .admonition-anchor-link {
|
||||
&:link, &:visited {
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
img/best-example.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
img/code-bug.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
img/complex-message.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
img/data-loss.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
img/no-title-bar.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
img/plain-note.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
2
integration/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/book
|
||||
/mdbook-admonish.css
|
||||
21
integration/book.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[book]
|
||||
authors = ["Tom Milligan"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "mdbook-admonish-integration"
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`
|
||||
after = ["links"]
|
||||
|
||||
[preprocessor.admonish.renderer.test]
|
||||
render_mode = "strip"
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["./mdbook-admonish.css"]
|
||||
21
integration/expected/book.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[book]
|
||||
authors = ["Tom Milligan"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "mdbook-admonish-integration"
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`
|
||||
after = ["links"]
|
||||
|
||||
[preprocessor.admonish.renderer.test]
|
||||
render_mode = "strip"
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["./mdbook-admonish.css"]
|
||||
93
integration/expected/chapter_1_main.html
Normal file
@@ -0,0 +1,93 @@
|
||||
<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:</p>
|
||||
<pre><code class="language-log">TOML parsing error: TOML parse error at line 1, column 8
|
||||
|
|
||||
1 | title="
|
||||
| ^
|
||||
invalid basic string
|
||||
|
||||
</code></pre>
|
||||
<p>Original markdown input:</p>
|
||||
<pre><code class="language-markdown">```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>
|
||||
<div id="admonition-warning" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
<p>Warning</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-warning"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<p>This is a commonly shared warning!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-note-2" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<p>Note</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note-2"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<pre><code class="language-bash">Nested code block
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-note-3" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<p>Note</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note-3"></a></p>
|
||||
</div>
|
||||
<div>
|
||||
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
|
||||
</span><span class="boring">fn main() {
|
||||
</span>let x = 10;
|
||||
x = 20;
|
||||
<span class="boring">}</span></code></pre></pre>
|
||||
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
|
||||
</span><span class="boring">fn main() {
|
||||
</span>let x = 10;
|
||||
let x = 20;
|
||||
<span class="boring">}</span></code></pre></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
71
integration/scripts/check
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/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/scripts/update-snapshot"
|
||||
eprintln "and commit the result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eprintln "Verifying mdbook test runs doctests"
|
||||
set +e
|
||||
TEST_RESULT="$(mdbook test 2>&1 | grep "1 passed; 1 failed")"
|
||||
set -e
|
||||
|
||||
if [[ "$TEST_RESULT" != "test result: FAILED. 1 passed; 1 failed;"* ]]; then
|
||||
eprintln ""
|
||||
eprintln "error: mdbook test did not complete as expected"
|
||||
eprintln ""
|
||||
eprintln "Full output:"
|
||||
mdbook test
|
||||
exit 1
|
||||
fi
|
||||
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
@@ -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
@@ -0,0 +1,3 @@
|
||||
# Summary
|
||||
|
||||
- [Chapter 1](./chapter_1.md)
|
||||
43
integration/src/chapter_1.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
{{#include common_warning.md}}
|
||||
|
||||
````admonish
|
||||
```bash
|
||||
Nested code block
|
||||
```
|
||||
````
|
||||
|
||||
````admonish
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
|
||||
```rust
|
||||
let x = 10;
|
||||
let x = 20;
|
||||
```
|
||||
````
|
||||
3
integration/src/common_warning.md
Normal file
@@ -0,0 +1,3 @@
|
||||
```admonish warning
|
||||
This is a commonly shared warning!
|
||||
```
|
||||
14
qvet.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
action:
|
||||
ready:
|
||||
type: link
|
||||
name: "Push New Version Tag"
|
||||
url: "https://github.com/tommilligan/mdbook-admonish"
|
||||
|
||||
commit:
|
||||
ignore:
|
||||
merges: true
|
||||
|
||||
release:
|
||||
identifiers:
|
||||
- type: tag
|
||||
pattern: "^v[0-9]"
|
||||
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"
|
||||
28
scripts/check
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
function eprintln() {
|
||||
>&2 echo "$1"
|
||||
}
|
||||
|
||||
eprintln "Formatting sources"
|
||||
cargo fmt -- --check
|
||||
|
||||
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
|
||||
13
scripts/install
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/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
|
||||
|
||||
./scripts/install-mdbook
|
||||
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 --version 0.4.32 --force
|
||||
fi
|
||||
13
scripts/publish
Executable 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
@@ -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
@@ -0,0 +1 @@
|
||||
^2.0.0
|
||||
1
src/bin/assets/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
2.0.2
|
||||
@@ -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,25 +59,43 @@ html :is(.admonition) > :last-child {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
:is(.admonition-title, summary) {
|
||||
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.admonition-title) {
|
||||
position: relative;
|
||||
min-height: 4rem;
|
||||
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;
|
||||
}
|
||||
html :is(.admonition-title, summary):last-child {
|
||||
:is(.admonition-title, summary.admonition-title) p {
|
||||
margin: 0;
|
||||
}
|
||||
html :is(.admonition-title, summary.admonition-title):last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:is(.admonition-title, summary)::before {
|
||||
:is(.admonition-title, summary.admonition-title)::before {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-start: 1.2rem;
|
||||
inset-inline-start: 1.6rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: #448aff;
|
||||
@@ -86,16 +107,39 @@ html :is(.admonition-title, summary):last-child {
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
}
|
||||
:is(.admonition-title, summary.admonition-title):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;
|
||||
}
|
||||
|
||||
:is(.note) > :is(.admonition-title, summary) {
|
||||
:is(.note) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
border-color: #448aff;
|
||||
}
|
||||
:is(.note) > :is(.admonition-title, summary)::before {
|
||||
:is(.note) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #448aff;
|
||||
mask-image: var(--md-admonition-icon--note);
|
||||
-webkit-mask-image: var(--md-admonition-icon--note);
|
||||
@@ -109,11 +153,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #00b0ff;
|
||||
}
|
||||
|
||||
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary) {
|
||||
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 176, 255, 0.1);
|
||||
border-color: #00b0ff;
|
||||
}
|
||||
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary)::before {
|
||||
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00b0ff;
|
||||
mask-image: var(--md-admonition-icon--abstract);
|
||||
-webkit-mask-image: var(--md-admonition-icon--abstract);
|
||||
@@ -127,11 +170,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #00b8d4;
|
||||
}
|
||||
|
||||
:is(.info, .todo) > :is(.admonition-title, summary) {
|
||||
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 184, 212, 0.1);
|
||||
border-color: #00b8d4;
|
||||
}
|
||||
:is(.info, .todo) > :is(.admonition-title, summary)::before {
|
||||
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00b8d4;
|
||||
mask-image: var(--md-admonition-icon--info);
|
||||
-webkit-mask-image: var(--md-admonition-icon--info);
|
||||
@@ -145,11 +187,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #00bfa5;
|
||||
}
|
||||
|
||||
:is(.tip, .hint, .important) > :is(.admonition-title, summary) {
|
||||
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 191, 165, 0.1);
|
||||
border-color: #00bfa5;
|
||||
}
|
||||
:is(.tip, .hint, .important) > :is(.admonition-title, summary)::before {
|
||||
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00bfa5;
|
||||
mask-image: var(--md-admonition-icon--tip);
|
||||
-webkit-mask-image: var(--md-admonition-icon--tip);
|
||||
@@ -163,11 +204,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #00c853;
|
||||
}
|
||||
|
||||
:is(.success, .check, .done) > :is(.admonition-title, summary) {
|
||||
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 200, 83, 0.1);
|
||||
border-color: #00c853;
|
||||
}
|
||||
:is(.success, .check, .done) > :is(.admonition-title, summary)::before {
|
||||
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00c853;
|
||||
mask-image: var(--md-admonition-icon--success);
|
||||
-webkit-mask-image: var(--md-admonition-icon--success);
|
||||
@@ -181,11 +221,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #64dd17;
|
||||
}
|
||||
|
||||
:is(.question, .help, .faq) > :is(.admonition-title, summary) {
|
||||
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(100, 221, 23, 0.1);
|
||||
border-color: #64dd17;
|
||||
}
|
||||
:is(.question, .help, .faq) > :is(.admonition-title, summary)::before {
|
||||
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #64dd17;
|
||||
mask-image: var(--md-admonition-icon--question);
|
||||
-webkit-mask-image: var(--md-admonition-icon--question);
|
||||
@@ -199,11 +238,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #ff9100;
|
||||
}
|
||||
|
||||
:is(.warning, .caution, .attention) > :is(.admonition-title, summary) {
|
||||
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 145, 0, 0.1);
|
||||
border-color: #ff9100;
|
||||
}
|
||||
:is(.warning, .caution, .attention) > :is(.admonition-title, summary)::before {
|
||||
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff9100;
|
||||
mask-image: var(--md-admonition-icon--warning);
|
||||
-webkit-mask-image: var(--md-admonition-icon--warning);
|
||||
@@ -217,11 +255,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #ff5252;
|
||||
}
|
||||
|
||||
:is(.failure, .fail, .missing) > :is(.admonition-title, summary) {
|
||||
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 82, 82, 0.1);
|
||||
border-color: #ff5252;
|
||||
}
|
||||
:is(.failure, .fail, .missing) > :is(.admonition-title, summary)::before {
|
||||
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff5252;
|
||||
mask-image: var(--md-admonition-icon--failure);
|
||||
-webkit-mask-image: var(--md-admonition-icon--failure);
|
||||
@@ -235,11 +272,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #ff1744;
|
||||
}
|
||||
|
||||
:is(.danger, .error) > :is(.admonition-title, summary) {
|
||||
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 23, 68, 0.1);
|
||||
border-color: #ff1744;
|
||||
}
|
||||
:is(.danger, .error) > :is(.admonition-title, summary)::before {
|
||||
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff1744;
|
||||
mask-image: var(--md-admonition-icon--danger);
|
||||
-webkit-mask-image: var(--md-admonition-icon--danger);
|
||||
@@ -253,11 +289,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #f50057;
|
||||
}
|
||||
|
||||
:is(.bug) > :is(.admonition-title, summary) {
|
||||
:is(.bug) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(245, 0, 87, 0.1);
|
||||
border-color: #f50057;
|
||||
}
|
||||
:is(.bug) > :is(.admonition-title, summary)::before {
|
||||
:is(.bug) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #f50057;
|
||||
mask-image: var(--md-admonition-icon--bug);
|
||||
-webkit-mask-image: var(--md-admonition-icon--bug);
|
||||
@@ -271,11 +306,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #7c4dff;
|
||||
}
|
||||
|
||||
:is(.example) > :is(.admonition-title, summary) {
|
||||
:is(.example) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(124, 77, 255, 0.1);
|
||||
border-color: #7c4dff;
|
||||
}
|
||||
:is(.example) > :is(.admonition-title, summary)::before {
|
||||
:is(.example) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #7c4dff;
|
||||
mask-image: var(--md-admonition-icon--example);
|
||||
-webkit-mask-image: var(--md-admonition-icon--example);
|
||||
@@ -289,11 +323,10 @@ html :is(.admonition-title, summary):last-child {
|
||||
border-color: #9e9e9e;
|
||||
}
|
||||
|
||||
:is(.quote, .cite) > :is(.admonition-title, summary) {
|
||||
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(158, 158, 158, 0.1);
|
||||
border-color: #9e9e9e;
|
||||
}
|
||||
:is(.quote, .cite) > :is(.admonition-title, summary)::before {
|
||||
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #9e9e9e;
|
||||
mask-image: var(--md-admonition-icon--quote);
|
||||
-webkit-mask-image: var(--md-admonition-icon--quote);
|
||||
@@ -315,3 +348,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,33 @@ 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.clone()).join(name);
|
||||
// Normalize path to remove no-op components
|
||||
// https://github.com/tommilligan/mdbook-admonish/issues/47
|
||||
let filepath: PathBuf = filepath.components().collect();
|
||||
let filepath_str = filepath.to_str().context("non-utf8 filepath")?;
|
||||
|
||||
if let Ok(ref mut additional_css) = additional_css {
|
||||
if !additional_css.contains_str(filepath_str) {
|
||||
@@ -150,18 +163,18 @@ mod install {
|
||||
"Copying '{name}' to '{filepath}'",
|
||||
filepath = filepath.display()
|
||||
);
|
||||
let mut file = File::create(filepath).expect("can't open file for writing");
|
||||
let mut file = File::create(&filepath).context("can't open file for writing")?;
|
||||
file.write_all(content)
|
||||
.expect("can't write content to file");
|
||||
.context("can't write content to file")?;
|
||||
}
|
||||
|
||||
let new_toml = doc.to_string();
|
||||
if new_toml != toml {
|
||||
log::info!("Saving changed configuration to '{}'", config.display());
|
||||
let mut file =
|
||||
File::create(config).expect("can't open configuration file for writing.");
|
||||
File::create(config).context("can't open configuration file for writing.")?;
|
||||
file.write_all(new_toml.as_bytes())
|
||||
.expect("can't write configuration");
|
||||
.context("can't write configuration")?;
|
||||
} else {
|
||||
log::info!("Configuration '{}' already up to date", config.display());
|
||||
}
|
||||
@@ -171,24 +184,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 +208,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.
|
||||
|
||||
60
src/book_config.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook::preprocess::PreprocessorContext;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::types::AdmonitionDefaults;
|
||||
|
||||
/// Loads the plugin configuration from mdbook internals.
|
||||
///
|
||||
/// Roundtrips config to string, to avoid linking the plugin's internal version of toml
|
||||
/// to the one publically exposed by the mdbook library.
|
||||
pub(crate) fn admonish_config_from_context(ctx: &PreprocessorContext) -> Result<Config> {
|
||||
let table: String = toml_mdbook::to_string(
|
||||
ctx.config
|
||||
.get_preprocessor("admonish")
|
||||
.context("No configuration for mdbook-admonish in book.toml")?,
|
||||
)?;
|
||||
toml::from_str(&table).context("Invalid mdbook-admonish configuration in book.toml")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct Config {
|
||||
#[serde(default)]
|
||||
pub on_failure: OnFailure,
|
||||
|
||||
#[serde(default)]
|
||||
pub default: AdmonitionDefaults,
|
||||
|
||||
#[serde(default)]
|
||||
pub renderer: HashMap<String, RendererConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub assets_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct RendererConfig {
|
||||
pub render_mode: Option<RenderMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum RenderMode {
|
||||
Preserve,
|
||||
Strip,
|
||||
Html,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum OnFailure {
|
||||
Bail,
|
||||
Continue,
|
||||
}
|
||||
|
||||
impl Default for OnFailure {
|
||||
fn default() -> Self {
|
||||
Self::Continue
|
||||
}
|
||||
}
|
||||
89
src/config/mod.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
mod v1;
|
||||
mod v2;
|
||||
|
||||
/// Configuration as described by the instance of an admonition in markdown.
|
||||
///
|
||||
/// This structure represents the configuration the user must provide in each
|
||||
/// instance.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct InstanceConfig {
|
||||
pub(crate) directive: String,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) additional_classnames: Vec<String>,
|
||||
pub(crate) collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
/// 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 InstanceConfig {
|
||||
/// Returns:
|
||||
/// - `None` if this is not an `admonish` block.
|
||||
/// - `Some(InstanceConfig)` if this is an `admonish` block
|
||||
pub fn from_info_string(info_string: &str) -> Option<Result<Self, String>> {
|
||||
let config_string = admonition_config_string(info_string)?;
|
||||
|
||||
// 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(config) = v1::from_config_string(config_string) {
|
||||
// If we succeed at parsing v1, return that.
|
||||
Ok(config)
|
||||
} else {
|
||||
// Otherwise return our v2 error.
|
||||
Err(config_v2_error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_from_info_string() {
|
||||
// Not admonition blocks
|
||||
assert_eq!(InstanceConfig::from_info_string(""), None);
|
||||
assert_eq!(InstanceConfig::from_info_string("adm"), None);
|
||||
// v1 syntax is supported back compatibly
|
||||
assert_eq!(
|
||||
InstanceConfig::from_info_string("admonish note.additional-classname")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: vec!["additional-classname".to_owned()],
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
// v2 syntax is supported
|
||||
assert_eq!(
|
||||
InstanceConfig::from_info_string(r#"admonish title="Custom Title" type="question""#)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "question".to_owned(),
|
||||
title: Some("Custom Title".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
130
src/config/v1.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use super::InstanceConfig;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig, String> {
|
||||
let config_string = config_string.trim();
|
||||
|
||||
static RX_CONFIG_STRING_V1: Lazy<Regex> = Lazy::new(|| {
|
||||
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(InstanceConfig {
|
||||
directive: directive.to_owned(),
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string() {
|
||||
assert_eq!(
|
||||
from_config_string("").unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(" ").unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("unknown").unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "unknown".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("note").unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("note.additional-classname").unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: vec!["additional-classname".to_owned()],
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[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()
|
||||
);
|
||||
}
|
||||
}
|
||||
179
src/config/v2.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use super::InstanceConfig;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct UserInput {
|
||||
#[serde(default)]
|
||||
r#type: Option<String>,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
#[serde(default)]
|
||||
class: Option<String>,
|
||||
#[serde(default)]
|
||||
collapsible: Option<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<InstanceConfig, String> {
|
||||
let config_toml = bare_key_value_pairs_to_toml(config_string);
|
||||
let config_toml = config_toml.trim();
|
||||
|
||||
let config: UserInput = match toml::from_str(config_toml) {
|
||||
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: UserInput = 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(InstanceConfig {
|
||||
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(),
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(" ").unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(
|
||||
r#"type="note" class="additional classname" title="Никита" collapsible=true"#
|
||||
)
|
||||
.unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: Some("Никита".to_owned()),
|
||||
additional_classnames: vec!["additional".to_owned(), "classname".to_owned()],
|
||||
collapsible: Some(true),
|
||||
}
|
||||
);
|
||||
// Specifying unknown keys is okay, as long as they're valid
|
||||
assert_eq!(
|
||||
from_config_string(r#"unkonwn="but valid toml""#).unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
// Just directive is fine
|
||||
assert_eq!(
|
||||
from_config_string(r#"info"#).unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
// Directive plus toml config
|
||||
assert_eq!(
|
||||
from_config_string(r#"info title="Information" collapsible=false"#).unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("Information".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: Some(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(),
|
||||
r#"TOML parsing error: TOML parse error at line 1, column 6
|
||||
|
|
||||
1 | note
|
||||
| ^
|
||||
expected `.`, `=`
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
||||
341
src/lib.rs
@@ -1,333 +1,10 @@
|
||||
use mdbook::book::{Book, BookItem, Chapter};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag};
|
||||
use std::str::FromStr;
|
||||
mod book_config;
|
||||
mod config;
|
||||
mod markdown;
|
||||
mod parse;
|
||||
mod preprocessor;
|
||||
mod render;
|
||||
mod resolve;
|
||||
mod types;
|
||||
|
||||
pub struct Admonish;
|
||||
|
||||
impl Preprocessor for Admonish {
|
||||
fn name(&self) -> &str {
|
||||
"admonish"
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
||||
let mut res = None;
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let Some(Err(_)) = res {
|
||||
return;
|
||||
}
|
||||
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
res = Some(Admonish::add_admonish(chapter).map(|md| {
|
||||
chapter.content = md;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
res.unwrap_or(Ok(())).map(|_| book)
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
renderer == "html"
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_html(s: &str) -> String {
|
||||
let mut output = String::new();
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'<' => output.push_str("<"),
|
||||
'>' => output.push_str(">"),
|
||||
'"' => output.push_str("""),
|
||||
'&' => output.push_str("&"),
|
||||
_ => output.push(c),
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
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) -> std::result::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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_info_string(info_string: &str) -> Option<Option<&str>> {
|
||||
if info_string == "admonish" {
|
||||
return Some(None);
|
||||
}
|
||||
|
||||
match info_string.split_once(' ') {
|
||||
Some(("admonish", directive)) => Some(Some(directive)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) -> Result<String> {
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(Options::ENABLE_TABLES);
|
||||
opts.insert(Options::ENABLE_FOOTNOTES);
|
||||
opts.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
opts.insert(Options::ENABLE_TASKLISTS);
|
||||
|
||||
let mut admonish_blocks = vec![];
|
||||
|
||||
let events = Parser::new_ext(content, opts);
|
||||
for (e, span) in events.into_offset_iter() {
|
||||
if let Event::Start(Tag::CodeBlock(Fenced(info_string))) = e.clone() {
|
||||
let directive_raw = match parse_info_string(info_string.as_ref()) {
|
||||
Some(directive) => directive,
|
||||
None => continue,
|
||||
};
|
||||
let directive = directive_raw
|
||||
.map(|directive| Directive::from_str(directive).ok())
|
||||
.flatten()
|
||||
.unwrap_or(Directive::Note);
|
||||
|
||||
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 = escape_html(admonish_content);
|
||||
let admonish_content = admonish_content.trim();
|
||||
let directive_title = directive_raw
|
||||
.map(ucfirst)
|
||||
.unwrap_or_else(|| "Note".to_owned());
|
||||
let directive_classname = match directive {
|
||||
Directive::Note => "note",
|
||||
Directive::Abstract => "abstract",
|
||||
Directive::Info => "info",
|
||||
Directive::Tip => "tip",
|
||||
Directive::Success => "success",
|
||||
Directive::Question => "question",
|
||||
Directive::Warning => "warning",
|
||||
Directive::Failure => "failure",
|
||||
Directive::Danger => "danger",
|
||||
Directive::Bug => "bug",
|
||||
Directive::Example => "example",
|
||||
Directive::Quote => "quote",
|
||||
};
|
||||
let admonish_code = format!(
|
||||
r#"<div class="admonition {directive_classname}">
|
||||
<p class="admonition-title">{directive_title}</p>
|
||||
<p>{admonish_content}</p>
|
||||
</div>"#
|
||||
);
|
||||
admonish_blocks.push((span, admonish_code.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut content = content.to_string();
|
||||
for (span, block) in admonish_blocks.iter().rev() {
|
||||
let pre_content = &content[..span.start];
|
||||
let post_content = &content[span.end..];
|
||||
content = format!("{}\n{}{}", pre_content, block, post_content);
|
||||
}
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
impl Admonish {
|
||||
fn add_admonish(chapter: &mut Chapter) -> Result<String> {
|
||||
add_admonish(&chapter.content)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
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("")));
|
||||
assert_eq!(parse_info_string("admonish unknown"), Some(Some("unknown")));
|
||||
assert_eq!(parse_info_string("admonish note"), Some(Some("note")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish() {
|
||||
let content = r#"# Chapter
|
||||
```admonish
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r#"# Chapter
|
||||
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>A simple admonition.</p>
|
||||
</div>
|
||||
Text
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, add_admonish(content).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive() {
|
||||
let content = r#"# Chapter
|
||||
```admonish warning
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r#"# Chapter
|
||||
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p>A simple admonition.</p>
|
||||
</div>
|
||||
Text
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, add_admonish(content).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_tables_untouched() {
|
||||
// Regression test.
|
||||
// Previously we forgot to enable the same markdwon extensions as mdbook itself.
|
||||
|
||||
let content = r#"# Heading
|
||||
| Head 1 | Head 2 |
|
||||
|--------|--------|
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
| Head 1 | Head 2 |
|
||||
|--------|--------|
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, add_admonish(content).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_html_untouched() {
|
||||
// Regression test.
|
||||
// Don't remove important newlines for syntax nested inside HTML
|
||||
|
||||
let content = r#"# Heading
|
||||
<del>
|
||||
*foo*
|
||||
</del>
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
<del>
|
||||
*foo*
|
||||
</del>
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, add_admonish(content).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_in_list() {
|
||||
// Regression test.
|
||||
// Don't remove important newlines for syntax nested inside HTML
|
||||
|
||||
let content = r#"# Heading
|
||||
1. paragraph 1
|
||||
```
|
||||
code 1
|
||||
```
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
1. paragraph 1
|
||||
```
|
||||
code 1
|
||||
```
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, add_admonish(content).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_in_admonish_block() {
|
||||
let content = r#"
|
||||
```admonish
|
||||
classDiagram
|
||||
class PingUploader {
|
||||
<<interface>>
|
||||
+Upload() UploadResult
|
||||
}
|
||||
```
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>classDiagram
|
||||
class PingUploader {
|
||||
<<interface>>
|
||||
+Upload() UploadResult
|
||||
}</p>
|
||||
</div>
|
||||
hello
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, add_admonish(content).unwrap());
|
||||
}
|
||||
}
|
||||
pub use crate::preprocessor::Admonish;
|
||||
|
||||
735
src/markdown.rs
Normal file
@@ -0,0 +1,735 @@
|
||||
use mdbook::errors::Result as MdbookResult;
|
||||
use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag};
|
||||
|
||||
pub use crate::preprocessor::Admonish;
|
||||
use crate::{
|
||||
book_config::OnFailure,
|
||||
parse::parse_admonition,
|
||||
types::{AdmonitionDefaults, RenderTextMode},
|
||||
};
|
||||
|
||||
pub(crate) fn preprocess(
|
||||
content: &str,
|
||||
on_failure: OnFailure,
|
||||
admonition_defaults: &AdmonitionDefaults,
|
||||
render_text_mode: RenderTextMode,
|
||||
) -> MdbookResult<String> {
|
||||
let mut id_counter = Default::default();
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(Options::ENABLE_TABLES);
|
||||
opts.insert(Options::ENABLE_FOOTNOTES);
|
||||
opts.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
opts.insert(Options::ENABLE_TASKLISTS);
|
||||
|
||||
let mut admonish_blocks = vec![];
|
||||
|
||||
let events = Parser::new_ext(content, opts);
|
||||
|
||||
for (event, span) in events.into_offset_iter() {
|
||||
if let Event::Start(Tag::CodeBlock(Fenced(info_string))) = event.clone() {
|
||||
let span_content = &content[span.start..span.end];
|
||||
|
||||
let admonition = match parse_admonition(
|
||||
info_string.as_ref(),
|
||||
admonition_defaults,
|
||||
span_content,
|
||||
on_failure,
|
||||
) {
|
||||
Some(admonition) => admonition,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let admonition = admonition?;
|
||||
|
||||
// Once we've identitified admonition blocks, handle them differently
|
||||
// depending on our render mode
|
||||
let new_content = match render_text_mode {
|
||||
RenderTextMode::Html => admonition.html_with_unique_ids(&mut id_counter),
|
||||
RenderTextMode::Strip => admonition.strip(),
|
||||
};
|
||||
|
||||
admonish_blocks.push((span, new_content));
|
||||
}
|
||||
}
|
||||
|
||||
let mut content = content.to_string();
|
||||
for (span, block) in admonish_blocks.iter().rev() {
|
||||
let pre_content = &content[..span.start];
|
||||
let post_content = &content[span.end..];
|
||||
content = format!("{}{}{}", pre_content, block, post_content);
|
||||
}
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn prep(content: &str) -> String {
|
||||
preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults::default(),
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish() {
|
||||
let content = r#"# Chapter
|
||||
```admonish
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_longer_code_fence() {
|
||||
let content = r#"# Chapter
|
||||
````admonish
|
||||
```json
|
||||
{}
|
||||
```
|
||||
````
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive() {
|
||||
let content = r#"# Chapter
|
||||
```admonish warning
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-warning" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Warning
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-warning"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive_alternate() {
|
||||
let content = r#"# Chapter
|
||||
```admonish caution
|
||||
A warning with alternate title.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-caution" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Caution
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-caution"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A warning with alternate title.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive_title() {
|
||||
let content = r#"# Chapter
|
||||
```admonish warning "Read **this**!"
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-read-this" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Read **this**!
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-read-this"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_tables_untouched() {
|
||||
// Regression test.
|
||||
// Previously we forgot to enable the same markdwon extensions as mdbook itself.
|
||||
|
||||
let content = r#"# Heading
|
||||
| Head 1 | Head 2 |
|
||||
|--------|--------|
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
| Head 1 | Head 2 |
|
||||
|--------|--------|
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_html_untouched() {
|
||||
// Regression test.
|
||||
// Don't remove important newlines for syntax nested inside HTML
|
||||
|
||||
let content = r#"# Heading
|
||||
<del>
|
||||
*foo*
|
||||
</del>
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
<del>
|
||||
*foo*
|
||||
</del>
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_in_list() {
|
||||
// Regression test.
|
||||
// Don't remove important newlines for syntax nested inside HTML
|
||||
|
||||
let content = r#"# Heading
|
||||
1. paragraph 1
|
||||
```
|
||||
code 1
|
||||
```
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
1. paragraph 1
|
||||
```
|
||||
code 1
|
||||
```
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_string_that_changes_length_when_parsed() {
|
||||
let content = r#"
|
||||
```admonish note "And \\"<i>in</i>\\" the title"
|
||||
With <b>html</b> styling.
|
||||
```
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-and-in-the-title" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
And "<i>in</i>" the title
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-and-in-the-title"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
With <b>html</b> styling.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
hello
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_string_ending_in_symbol() {
|
||||
let content = r#"
|
||||
```admonish warning "Trademark™"
|
||||
Should be respected
|
||||
```
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-trademark" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
|
||||
Trademark™
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-trademark"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Should be respected
|
||||
|
||||
</div>
|
||||
</div>
|
||||
hello
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_additional_classname() {
|
||||
let content = r#"
|
||||
```admonish tip.my-style.other-style
|
||||
Will have bonus classnames
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-tip" class="admonition tip my-style other-style">
|
||||
<div class="admonition-title">
|
||||
|
||||
Tip
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-tip"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Will have bonus classnames
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_additional_classname_and_title() {
|
||||
let content = r#"
|
||||
```admonish tip.my-style.other-style "Developers don't want you to know this one weird tip!"
|
||||
Will have bonus classnames
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-developers-dont-want-you-to-know-this-one-weird-tip" class="admonition tip my-style other-style">
|
||||
<div class="admonition-title">
|
||||
|
||||
Developers don't want you to know this one weird tip!
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-developers-dont-want-you-to-know-this-one-weird-tip"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Will have bonus classnames
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_empty_additional_classnames_title_content() {
|
||||
let content = r#"
|
||||
```admonish .... ""
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
|
||||
<div id="admonition-default" class="admonition note">
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unique_ids_same_title() {
|
||||
let content = r#"
|
||||
```admonish note "My Note"
|
||||
Content zero.
|
||||
```
|
||||
|
||||
```admonish note "My Note"
|
||||
Content one.
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-my-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
My Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-my-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Content zero.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="admonition-my-note-1" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
My Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-my-note-1"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Content one.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_config_works() {
|
||||
let content = r#"
|
||||
```admonish tip class="my other-style" title="Article Heading"
|
||||
Bonus content!
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-article-heading" class="admonition tip my other-style">
|
||||
<div class="admonition-title">
|
||||
|
||||
Article Heading
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-article-heading"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Bonus content!
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continue_on_error_output() {
|
||||
let content = r#"
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-error-rendering-admonishment" class="admonition bug">
|
||||
<div class="admonition-title">
|
||||
|
||||
Error rendering admonishment
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Failed with:
|
||||
|
||||
```log
|
||||
TOML parsing error: TOML parse error at line 1, column 8
|
||||
|
|
||||
1 | title="
|
||||
| ^
|
||||
invalid basic string
|
||||
|
||||
```
|
||||
|
||||
Original markdown input:
|
||||
|
||||
````markdown
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bail_on_error_output() {
|
||||
let content = r#"
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```
|
||||
"#;
|
||||
assert_eq!(
|
||||
preprocess(
|
||||
content,
|
||||
OnFailure::Bail,
|
||||
&AdmonitionDefaults::default(),
|
||||
RenderTextMode::Html
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
r#"Error processing admonition, bailing:
|
||||
```admonish title="
|
||||
Bonus content!
|
||||
```"#
|
||||
.to_owned()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renderer_strip_explicit() {
|
||||
let content = r#"
|
||||
````admonish title="Title"
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
````
|
||||
"#;
|
||||
assert_eq!(
|
||||
preprocess(
|
||||
content,
|
||||
OnFailure::Bail,
|
||||
&AdmonitionDefaults::default(),
|
||||
RenderTextMode::Strip
|
||||
)
|
||||
.unwrap(),
|
||||
r#"
|
||||
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
|
||||
"#
|
||||
.to_owned()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_collapsible() {
|
||||
let content = r#"
|
||||
```admonish collapsible=true
|
||||
Hidden
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<details id="admonition-note" class="admonition note">
|
||||
<summary class="admonition-title">
|
||||
|
||||
Note
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</summary>
|
||||
<div>
|
||||
|
||||
Hidden
|
||||
|
||||
</div>
|
||||
</details>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_toml_title() {
|
||||
let content = r#"# Chapter
|
||||
```admonish
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-admonish" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
Admonish
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-admonish"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
let preprocess_result = preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Admonish".to_owned()),
|
||||
collapsible: false,
|
||||
},
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(expected, preprocess_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_explicit_title_with_default() {
|
||||
let content = r#"# Chapter
|
||||
```admonish title=""
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r#"# Chapter
|
||||
|
||||
<div id="admonition-default" class="admonition note">
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"#;
|
||||
|
||||
let preprocess_result = preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Admonish".to_owned()),
|
||||
collapsible: false,
|
||||
},
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(expected, preprocess_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_explicit_title() {
|
||||
let content = r#"# Chapter
|
||||
```admonish title=""
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r#"# Chapter
|
||||
|
||||
<div id="admonition-default" class="admonition note">
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
}
|
||||
223
src/parse.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::preprocessor::Admonish;
|
||||
use crate::{
|
||||
book_config::OnFailure,
|
||||
render::Admonition,
|
||||
resolve::AdmonitionMeta,
|
||||
types::{AdmonitionDefaults, Directive},
|
||||
};
|
||||
|
||||
/// Given the content in the span of the code block, and the info string,
|
||||
/// return `Some(Admonition)` if the code block is an admonition.
|
||||
///
|
||||
/// If there is an error parsing the admonition, either:
|
||||
///
|
||||
/// - Display a UI error message output in the book.
|
||||
/// - If configured, break the build.
|
||||
///
|
||||
/// If the code block is not an admonition, return `None`.
|
||||
pub(crate) fn parse_admonition<'a>(
|
||||
info_string: &'a str,
|
||||
admonition_defaults: &'a AdmonitionDefaults,
|
||||
content: &'a str,
|
||||
on_failure: OnFailure,
|
||||
) -> Option<Result<Admonition<'a>>> {
|
||||
// We need to know fence details anyway for error messages
|
||||
let extracted = extract_admonish_body(content);
|
||||
|
||||
let info = AdmonitionMeta::from_info_string(info_string, admonition_defaults)?;
|
||||
let info = match info {
|
||||
Ok(info) => info,
|
||||
// FIXME return error messages to break build if configured
|
||||
// Err(message) => return Some(Err(content)),
|
||||
Err(message) => {
|
||||
// Construct a fence capable of enclosing whatever we wrote for the
|
||||
// actual input block
|
||||
let fence = extracted.fence;
|
||||
let enclosing_fence: String = std::iter::repeat(fence.character)
|
||||
.take(fence.length + 1)
|
||||
.collect();
|
||||
return Some(match on_failure {
|
||||
OnFailure::Continue => {
|
||||
log::warn!(
|
||||
r#"Error processing admonition. To fail the build instead of continuing, set 'on_failure = "bail"'"#
|
||||
);
|
||||
Ok(Admonition {
|
||||
directive: Directive::Bug,
|
||||
title: "Error rendering admonishment".to_owned(),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
content: Cow::Owned(format!(
|
||||
r#"Failed with:
|
||||
|
||||
```log
|
||||
{message}
|
||||
```
|
||||
|
||||
Original markdown input:
|
||||
|
||||
{enclosing_fence}markdown
|
||||
{content}
|
||||
{enclosing_fence}
|
||||
"#
|
||||
)),
|
||||
})
|
||||
}
|
||||
OnFailure::Bail => Err(anyhow!("Error processing admonition, bailing:\n{content}")),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Some(Ok(Admonition::new(info, extracted.body)))
|
||||
}
|
||||
|
||||
/// We can't trust the info string length to find the start of the body
|
||||
/// it may change length if it contains HTML or character escapes.
|
||||
///
|
||||
/// So we scan for the first newline and use that.
|
||||
/// If gods forbid it doesn't exist for some reason, just include the whole info string.
|
||||
fn extract_admonish_body_start_index(content: &str) -> usize {
|
||||
let index = content
|
||||
.find('\n')
|
||||
// Start one character _after_ the newline
|
||||
.map(|index| index + 1);
|
||||
|
||||
// If we can't get a valid index, include all content
|
||||
match index {
|
||||
// Couldn't find a newline
|
||||
None => 0,
|
||||
Some(index) => {
|
||||
// Index out of bound of content
|
||||
if index > (content.len() - 1) {
|
||||
0
|
||||
} else {
|
||||
index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_admonish_body_end_index(content: &str) -> (usize, Fence) {
|
||||
let fence_character = content.chars().next_back().unwrap_or('`');
|
||||
let number_fence_characters = content
|
||||
.chars()
|
||||
.rev()
|
||||
.position(|c| c != fence_character)
|
||||
.unwrap_or_default();
|
||||
let fence = Fence::new(fence_character, number_fence_characters);
|
||||
|
||||
let index = content.len() - fence.length;
|
||||
(index, fence)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Fence {
|
||||
character: char,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl Fence {
|
||||
fn new(character: char, length: usize) -> Self {
|
||||
Self { character, length }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Extracted<'a> {
|
||||
body: &'a str,
|
||||
fence: Fence,
|
||||
}
|
||||
|
||||
/// Given the whole text content of the code fence, extract the body.
|
||||
///
|
||||
/// This really feels like we should get the markdown parser to do it for us,
|
||||
/// but it's not really clear a good way of doing that.
|
||||
///
|
||||
/// ref: https://spec.commonmark.org/0.30/#fenced-code-blocks
|
||||
fn extract_admonish_body(content: &str) -> Extracted<'_> {
|
||||
let start_index = extract_admonish_body_start_index(content);
|
||||
let (end_index, fence) = extract_admonish_body_end_index(content);
|
||||
|
||||
let admonish_content = &content[start_index..end_index];
|
||||
// The newline after a code block is technically optional, so we have to
|
||||
// trim it off dynamically.
|
||||
let body = admonish_content.trim_end();
|
||||
Extracted { body, fence }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_extract_start() {
|
||||
for (text, expected) in [
|
||||
("```sane example\ncontent```", 16),
|
||||
("~~~~~\nlonger fence", 6),
|
||||
// empty
|
||||
("```\n```", 4),
|
||||
// bounds check, should not index outside of content
|
||||
("```\n", 0),
|
||||
] {
|
||||
let actual = extract_admonish_body_start_index(text);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_end() {
|
||||
for (text, expected) in [
|
||||
("\n```", (1, Fence::new('`', 3))),
|
||||
// different lengths
|
||||
("\n``````", (1, Fence::new('`', 6))),
|
||||
("\n~~~~", (1, Fence::new('~', 4))),
|
||||
// whitespace before fence end
|
||||
("\n ```", (4, Fence::new('`', 3))),
|
||||
("content\n```", (8, Fence::new('`', 3))),
|
||||
] {
|
||||
let actual = extract_admonish_body_end_index(text);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract() {
|
||||
fn content_fence(body: &'static str, character: char, length: usize) -> Extracted<'static> {
|
||||
Extracted {
|
||||
body,
|
||||
fence: Fence::new(character, length),
|
||||
}
|
||||
}
|
||||
for (text, expected) in [
|
||||
// empty
|
||||
("```\n```", content_fence("", '`', 3)),
|
||||
// standard
|
||||
(
|
||||
"```admonish\ncontent\n```",
|
||||
content_fence("content", '`', 3),
|
||||
),
|
||||
// whitespace
|
||||
(
|
||||
"```admonish \n content \n ```",
|
||||
content_fence(" content", '`', 3),
|
||||
),
|
||||
// longer
|
||||
(
|
||||
"``````admonish\ncontent\n``````",
|
||||
content_fence("content", '`', 6),
|
||||
),
|
||||
// unequal
|
||||
(
|
||||
"~~~admonish\ncontent\n~~~~~",
|
||||
// longer (end) fence returned
|
||||
content_fence("content", '~', 5),
|
||||
),
|
||||
] {
|
||||
let actual = extract_admonish_body(text);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
259
src/preprocessor.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use mdbook::{
|
||||
book::{Book, BookItem},
|
||||
errors::Result as MdbookResult,
|
||||
preprocess::{Preprocessor, PreprocessorContext},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
book_config::{admonish_config_from_context, Config, RenderMode},
|
||||
markdown::preprocess,
|
||||
types::RenderTextMode,
|
||||
};
|
||||
|
||||
pub struct Admonish;
|
||||
|
||||
impl Preprocessor for Admonish {
|
||||
fn name(&self) -> &str {
|
||||
"admonish"
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> MdbookResult<Book> {
|
||||
let config = admonish_config_from_context(ctx)?;
|
||||
ensure_compatible_assets_version(&config)?;
|
||||
|
||||
let on_failure = config.on_failure;
|
||||
let admonition_defaults = config.default;
|
||||
|
||||
// Load what rendering we should do from config, falling back to a default
|
||||
let render_mode = config
|
||||
.renderer
|
||||
.get(&ctx.renderer)
|
||||
.and_then(|renderer| renderer.render_mode)
|
||||
.unwrap_or_else(|| {
|
||||
// By default only render html for the html renderer
|
||||
// For everything else, do nothing
|
||||
if &ctx.renderer == "html" {
|
||||
RenderMode::Html
|
||||
} else {
|
||||
RenderMode::Preserve
|
||||
}
|
||||
});
|
||||
let render_text_mode = match render_mode {
|
||||
RenderMode::Preserve => return Ok(book),
|
||||
RenderMode::Html => RenderTextMode::Html,
|
||||
RenderMode::Strip => RenderTextMode::Strip,
|
||||
};
|
||||
|
||||
let mut res = None;
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let Some(Err(_)) = res {
|
||||
return;
|
||||
}
|
||||
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
res = Some(
|
||||
preprocess(
|
||||
&chapter.content,
|
||||
on_failure,
|
||||
&admonition_defaults,
|
||||
render_text_mode,
|
||||
)
|
||||
.map(|md| {
|
||||
chapter.content = md;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
res.unwrap_or(Ok(())).map(|_| book)
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, _renderer: &str) -> bool {
|
||||
// We support all renderers, but will only actually take action
|
||||
// if configured to do so - or, if it's the html renderer
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_compatible_assets_version(config: &Config) -> Result<()> {
|
||||
use semver::{Version, VersionReq};
|
||||
|
||||
const REQUIRES_ASSETS_VERSION: &str = std::include_str!("./REQUIRED_ASSETS_VERSION");
|
||||
let requirement = VersionReq::parse(REQUIRES_ASSETS_VERSION.trim()).unwrap();
|
||||
|
||||
const USER_ACTION: &str = "Please run `mdbook-admonish install` to update installed assets.";
|
||||
const DOCS_REFERENCE: &str = "For more information, see: https://github.com/tommilligan/mdbook-admonish#semantic-versioning";
|
||||
|
||||
let version = match &config.assets_version {
|
||||
Some(version) => version,
|
||||
None => {
|
||||
return Err(anyhow!(
|
||||
r#"ERROR:
|
||||
Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but did not find a version.
|
||||
{USER_ACTION}
|
||||
{DOCS_REFERENCE}"#
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let version = Version::parse(version).unwrap();
|
||||
|
||||
if !requirement.matches(&version) {
|
||||
return Err(anyhow!(
|
||||
r#"ERROR:
|
||||
Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but found '{version}'.
|
||||
{USER_ACTION}
|
||||
{DOCS_REFERENCE}"#
|
||||
));
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn mock_book(content: &str) -> Book {
|
||||
serde_json::from_value(json!({
|
||||
"sections": [
|
||||
{
|
||||
"Chapter": {
|
||||
"name": "Chapter 1",
|
||||
"content": content,
|
||||
"number": [1],
|
||||
"sub_items": [],
|
||||
"path": "chapter_1.md",
|
||||
"source_path": "chapter_1.md",
|
||||
"parent_names": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"__non_exhaustive": null
|
||||
}))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn mock_context(admonish: &Value, renderer: &str) -> PreprocessorContext {
|
||||
let value = json!({
|
||||
"root": "/path/to/book",
|
||||
"config": {
|
||||
"book": {
|
||||
"authors": ["AUTHOR"],
|
||||
"language": "en",
|
||||
"multilingual": false,
|
||||
"src": "src",
|
||||
"title": "TITLE"
|
||||
},
|
||||
"preprocessor": {
|
||||
"admonish": admonish,
|
||||
}
|
||||
},
|
||||
"renderer": renderer,
|
||||
"mdbook_version": "0.4.21"
|
||||
});
|
||||
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_html() {
|
||||
let content = r#"
|
||||
````admonish title="Title"
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
````
|
||||
"#;
|
||||
let expected_content = r##"
|
||||
|
||||
<div id="admonition-title" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
|
||||
Title
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-title"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
let ctx = mock_context(
|
||||
&json!({
|
||||
"assets_version": "2.0.0"
|
||||
}),
|
||||
"html",
|
||||
);
|
||||
let book = mock_book(content);
|
||||
let expected_book = mock_book(expected_content);
|
||||
|
||||
assert_eq!(Admonish.run(&ctx, book).unwrap(), expected_book)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_test_preserves_by_default() {
|
||||
let content = r#"
|
||||
````admonish title="Title"
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
````
|
||||
"#;
|
||||
let ctx = mock_context(
|
||||
&json!({
|
||||
"assets_version": "2.0.0"
|
||||
}),
|
||||
"test",
|
||||
);
|
||||
let book = mock_book(content);
|
||||
let expected_book = book.clone();
|
||||
|
||||
assert_eq!(Admonish.run(&ctx, book).unwrap(), expected_book)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_test_can_strip() {
|
||||
let content = r#"
|
||||
````admonish title="Title"
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
````
|
||||
"#;
|
||||
let expected_content = r#"
|
||||
|
||||
```rust
|
||||
let x = 10;
|
||||
x = 20;
|
||||
```
|
||||
|
||||
"#;
|
||||
let ctx = mock_context(
|
||||
&json!({
|
||||
"assets_version": "2.0.0",
|
||||
"renderer": {
|
||||
"test": {
|
||||
"render_mode": "strip",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"test",
|
||||
);
|
||||
let book = mock_book(content);
|
||||
let expected_book = mock_book(expected_content);
|
||||
|
||||
assert_eq!(Admonish.run(&ctx, book).unwrap(), expected_book)
|
||||
}
|
||||
}
|
||||
122
src/render.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use mdbook::utils::unique_id_from_content;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use crate::preprocessor::Admonish;
|
||||
use crate::{resolve::AdmonitionMeta, types::Directive};
|
||||
|
||||
impl Directive {
|
||||
fn classname(&self) -> &'static str {
|
||||
match self {
|
||||
Directive::Note => "note",
|
||||
Directive::Abstract => "abstract",
|
||||
Directive::Info => "info",
|
||||
Directive::Tip => "tip",
|
||||
Directive::Success => "success",
|
||||
Directive::Question => "question",
|
||||
Directive::Warning => "warning",
|
||||
Directive::Failure => "failure",
|
||||
Directive::Danger => "danger",
|
||||
Directive::Bug => "bug",
|
||||
Directive::Example => "example",
|
||||
Directive::Quote => "quote",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Admonition<'a> {
|
||||
pub(crate) directive: Directive,
|
||||
pub(crate) title: String,
|
||||
pub(crate) content: Cow<'a, str>,
|
||||
pub(crate) additional_classnames: Vec<String>,
|
||||
pub(crate) collapsible: bool,
|
||||
}
|
||||
|
||||
impl<'a> Admonition<'a> {
|
||||
pub(crate) fn new(info: AdmonitionMeta, content: &'a str) -> Self {
|
||||
let AdmonitionMeta {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
} = info;
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
content: Cow::Borrowed(content),
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn html_with_unique_ids(&self, id_counter: &mut HashMap<String, usize>) -> String {
|
||||
let anchor_id = unique_id_from_content(
|
||||
if !self.title.is_empty() {
|
||||
&self.title
|
||||
} else {
|
||||
ANCHOR_ID_DEFAULT
|
||||
},
|
||||
id_counter,
|
||||
);
|
||||
self.html(&anchor_id)
|
||||
}
|
||||
|
||||
fn html(&self, anchor_id: &str) -> String {
|
||||
let mut additional_class = Cow::Borrowed(self.directive.classname());
|
||||
let title = &self.title;
|
||||
let content = &self.content;
|
||||
|
||||
let title_block = if self.collapsible { "summary" } else { "div" };
|
||||
|
||||
let title_html = if !title.is_empty() {
|
||||
Cow::Owned(format!(
|
||||
r##"<{title_block} class="admonition-title">
|
||||
|
||||
{title}
|
||||
|
||||
<a class="admonition-anchor-link" href="#{ANCHOR_ID_PREFIX}-{anchor_id}"></a>
|
||||
</{title_block}>
|
||||
"##
|
||||
))
|
||||
} else {
|
||||
Cow::Borrowed("")
|
||||
};
|
||||
|
||||
if !self.additional_classnames.is_empty() {
|
||||
let mut buffer = additional_class.into_owned();
|
||||
for additional_classname in &self.additional_classnames {
|
||||
buffer.push(' ');
|
||||
buffer.push_str(additional_classname);
|
||||
}
|
||||
|
||||
additional_class = Cow::Owned(buffer);
|
||||
}
|
||||
|
||||
let admonition_block = if self.collapsible { "details" } else { "div" };
|
||||
// Notes on the HTML template:
|
||||
// - the additional whitespace around the content are deliberate
|
||||
// In line with the commonmark spec, this allows the inner content to be
|
||||
// rendered as markdown paragraphs.
|
||||
format!(
|
||||
r#"
|
||||
<{admonition_block} id="{ANCHOR_ID_PREFIX}-{anchor_id}" class="admonition {additional_class}">
|
||||
{title_html}<div>
|
||||
|
||||
{content}
|
||||
|
||||
</div>
|
||||
</{admonition_block}>"#,
|
||||
)
|
||||
}
|
||||
|
||||
/// Strips all admonish syntax, leaving the plain content of the block.
|
||||
pub(crate) fn strip(&self) -> String {
|
||||
// Add in newlines to preserve line numbering for test output
|
||||
// These replace the code fences we stripped out
|
||||
format!("\n{}\n", self.content)
|
||||
}
|
||||
}
|
||||
|
||||
const ANCHOR_ID_PREFIX: &str = "admonition";
|
||||
const ANCHOR_ID_DEFAULT: &str = "default";
|
||||
116
src/resolve.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use crate::config::InstanceConfig;
|
||||
use crate::types::{AdmonitionDefaults, Directive};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// All information required to render an admonition.
|
||||
///
|
||||
/// i.e. all configured options have been resolved at this point.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct AdmonitionMeta {
|
||||
pub directive: Directive,
|
||||
pub title: String,
|
||||
pub additional_classnames: Vec<String>,
|
||||
pub collapsible: bool,
|
||||
}
|
||||
|
||||
impl AdmonitionMeta {
|
||||
pub fn from_info_string(
|
||||
info_string: &str,
|
||||
defaults: &AdmonitionDefaults,
|
||||
) -> Option<Result<Self, String>> {
|
||||
InstanceConfig::from_info_string(info_string)
|
||||
.map(|raw| raw.map(|raw| Self::resolve(raw, defaults)))
|
||||
}
|
||||
|
||||
/// Combine the per-admonition configuration with global defaults (and
|
||||
/// other logic) to resolve the values needed for rendering.
|
||||
fn resolve(raw: InstanceConfig, defaults: &AdmonitionDefaults) -> Self {
|
||||
let InstanceConfig {
|
||||
directive: raw_directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
} = raw;
|
||||
|
||||
// Use values from block, else load default value
|
||||
let title = title.or_else(|| defaults.title.clone());
|
||||
let collapsible = collapsible.unwrap_or(defaults.collapsible);
|
||||
|
||||
// Load the directive (and title, if one still not given)
|
||||
let (directive, title) = match (Directive::from_str(&raw_directive), title) {
|
||||
(Ok(directive), None) => (directive, ucfirst(&raw_directive)),
|
||||
(Err(_), None) => (Directive::Note, "Note".to_owned()),
|
||||
(Ok(directive), Some(title)) => (directive, title),
|
||||
(Err(_), Some(title)) => (Directive::Note, title),
|
||||
};
|
||||
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the first letter of `input` upppercase.
|
||||
///
|
||||
/// source: https://stackoverflow.com/a/38406885
|
||||
fn ucfirst(input: &str) -> String {
|
||||
let mut chars = input.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: " ".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Default::default()
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: Directive::Note,
|
||||
title: "Note".to_owned(),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw_with_defaults() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: " ".to_owned(),
|
||||
title: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Important!!!".to_owned()),
|
||||
collapsible: true,
|
||||
},
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: Directive::Note,
|
||||
title: "Important!!!".to_owned(),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
56
src/types.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Book wide defaults that may be provided by the user.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
|
||||
pub(crate) struct AdmonitionDefaults {
|
||||
#[serde(default)]
|
||||
pub(crate) title: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) collapsible: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum Directive {
|
||||
Note,
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum RenderTextMode {
|
||||
Strip,
|
||||
Html,
|
||||
}
|
||||