Compare commits

..

128 Commits

Author SHA1 Message Date
Tom Milligan
0304995dbb ci: add msrv to cargo toml, update mdbook version 2023-10-15 13:11:01 +01:00
Tom Milligan
197d9cd059 Merge pull request #140 from tommilligan/prep-1.13.0
chore: prep v0.13.0 release
2023-10-06 15:21:27 +01:00
Tom Milligan
04ff932f1f chore: prep v0.13.0 release 2023-10-06 14:52:13 +01:00
Tom Milligan
1526a5d814 Merge pull request #139 from tommilligan/pr-137
prefix directive class names with 'admonish-'
2023-10-06 14:16:45 +01:00
phoenixr-codes
eb21495797 prefix directive class names with 'admonish-'
This change exists to prevent conflicts with the newly added 'warning' class by mdBook. See also: https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md#added
2023-10-06 13:43:54 +01:00
Tom Milligan
ebe6f7815c Merge pull request #138 from tommilligan/ci-prettier
ci: lint styles with prettier
2023-10-06 13:32:48 +01:00
Tom Milligan
c0c953c865 ci: lint styles with prettier 2023-10-06 13:15:45 +01:00
Tom Milligan
0fa34a66a0 Merge pull request #136 from tommilligan/deps-20231001
chore: deps 20231001
2023-10-02 08:50:53 +01:00
dependabot[bot]
dfc12c3652 chore(deps): bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-02 08:37:26 +01:00
dependabot[bot]
108edfffc5 chore(deps): bump JamesIves/github-pages-deploy-action
Bumps [JamesIves/github-pages-deploy-action](https://github.com/jamesives/github-pages-deploy-action) from 4.4.1 to 4.4.3.
- [Release notes](https://github.com/jamesives/github-pages-deploy-action/releases)
- [Commits](https://github.com/jamesives/github-pages-deploy-action/compare/v4.4.1...v4.4.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-02 08:37:26 +01:00
Tom Milligan
38f1933e8f chore: bump rust deps 2023-10-02 08:37:26 +01:00
Tom Milligan
a839038263 Merge pull request #128 from tommilligan/bug-127
markdown: fix panic when searching for indent
2023-09-19 13:02:43 +01:00
Tom Milligan
8501c812d9 chore: prep v1.12.1 2023-09-19 12:21:42 +01:00
Tom Milligan
933432afb2 markdown: fix panic when searching for indent 2023-09-19 12:21:42 +01:00
Tom Milligan
496e8f7c6d Merge pull request #126 from tommilligan/prep-1.12.0
chore: prep v1.12.0 release
2023-09-16 11:22:08 +01:00
Tom Milligan
1877fd1731 chore: upgrade internal dependencies 2023-09-16 10:58:17 +01:00
Tom Milligan
20286b3fae chore: prep v1.12.0 release 2023-09-16 10:55:53 +01:00
Tom Milligan
8e68cf919f Merge pull request #124 from tommilligan/bug-123
feat: support admonitions inside list items
2023-09-10 00:02:58 +01:00
Tom Milligan
02640dab1f feat: support admonitions inside list items 2023-09-09 23:44:58 +01:00
Tom Milligan
771e9c9fd8 Merge pull request #125 from tommilligan/prep-v1.11.1
chore: prepare v1.11.1 release
2023-09-09 23:43:48 +01:00
Tom Milligan
cce9343c47 chore: prepare v1.11.1 release 2023-09-09 23:32:10 +01:00
Tom Milligan
20b158966b Revert "chore: bump lockfile"
This reverts commit 39edc4d92a.
2023-09-09 23:24:26 +01:00
Tom Milligan
491f9cf341 Merge pull request #122 from tommilligan/fix-docs
docs: fix mdbook-toc build failure
2023-09-09 09:12:27 +01:00
Tom Milligan
6deaf1ea2b docs: fix mdbook-toc build failure 2023-09-09 09:12:07 +01:00
Tom Milligan
041e5a566f Merge pull request #121 from tommilligan/update-deps
chore: prep v1.11.0 release
2023-09-09 09:06:44 +01:00
Tom Milligan
99b5a235cf chore: prep v1.11.0 release 2023-09-09 09:05:49 +01:00
Tom Milligan
39edc4d92a chore: bump lockfile 2023-09-09 08:58:32 +01:00
Tom Milligan
7773213093 Merge pull request #118 from eitsupi/eitsupi-patch-1
ci(deploy): Use Ubuntu 20.04 for building linux gnu target binaries
2023-09-09 08:49:24 +01:00
eitsupi
e888fcd021 Use Ubuntu 20.04 for building linux gnu target binaries 2023-09-09 08:48:24 +01:00
Tom Milligan
95dc7582ad Merge pull request #119 from eitsupi/add-aarch64-musl
ci(deploy): deploy aarch64-unknown-linux-musl
2023-09-09 08:43:59 +01:00
eitsupi
b658eb6049 strip via Cargo 2023-09-09 08:31:25 +01:00
eitsupi
623291625a deploy aarch64-unknown-linux-musl 2023-09-09 08:31:25 +01:00
Tom Milligan
4dad5a86c8 chore: fix clippy 2023-09-09 08:28:21 +01:00
Tom Milligan
7e774f4655 chore: prep v1.10.2 release (#116) 2023-08-07 18:58:32 +01:00
Tom Milligan
823cefbcbc fix: unlink mdbook internal toml dependency from mdbook-admonish (#115)
* chore: bump dependencies

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

* regenerate assets, update snapshot test, fix themes

---------

Co-authored-by: Tom Milligan <tom.milligan@uipath.com>
2023-07-28 18:44:05 +01:00
Jonas
afdc2b03d0 Improvement of links (#111) 2023-07-26 12:19:01 +01:00
Tom Milligan
e55df3e60b Merge pull request #110 from tommilligan/prep-1.10.0
chore: prep v1.10.0 release
2023-07-23 19:51:53 +01:00
Tom Milligan
f3d49b93de chore: prep v1.10.0 release 2023-07-23 19:47:37 +01:00
Tom Milligan
92caf95b34 chore: add notes for future v2 release 2023-07-23 19:38:24 +01:00
Tom Milligan
0742c6c1e8 Merge pull request #109 from tommilligan/run-doctests
feat: add support for test renderer, running doctests
2023-07-23 19:33:20 +01:00
Tom Milligan
60706be3e0 ci: bump MSRV to 1.66.0 2023-07-23 19:21:18 +01:00
Tom Milligan
f5a6b9ef0f ci: build docs on each merge 2023-07-23 19:13:10 +01:00
Tom Milligan
9361b7e7fa book: add configuration docs 2023-07-23 19:10:24 +01:00
Tom Milligan
24bef47b15 internal: restructure book.toml config parsing 2023-07-23 19:10:24 +01:00
Tom Milligan
0eb5fd35c3 internal: restructure configuration parsing 2023-07-23 17:53:19 +01:00
Tom Milligan
de539cd0fd internal: split up lib.rs 2023-07-22 11:32:40 +01:00
Tom Milligan
4842daea1c feat: add support for test renderer, running doctests 2023-07-22 10:44:11 +01:00
Tom Milligan
76212fccfb internal: refactor how whitespace is added 2023-07-22 10:43:47 +01:00
Tom Milligan
681c991a9a Merge pull request #98 from tommilligan/ci-publish-auto
ci: fix automatic crates.io publication
2023-05-05 09:52:54 +01:00
Tom Milligan
1e0a3992d5 ci: fix automatic crates.io publication 2023-05-05 09:16:28 +01:00
Tom Milligan
d97747d195 Merge pull request #91 from tommilligan/prep-1.9.0
chore: prep v1.9.0 release
2023-04-23 14:25:53 +01:00
Tom Milligan
2e2cebfc83 chore: prep v1.9.0 release 2023-04-23 14:11:53 +01:00
Tom Milligan
ab595f18f7 Merge pull request #90 from tommilligan/qvet
ci: add qvet config
2023-04-23 14:01:57 +01:00
Tom Milligan
196585f4f2 ci: add qvet config 2023-04-23 14:01:38 +01:00
Tom Milligan
7ad4d3f18c Merge pull request #89 from tommilligan/error-fences
fix: ensure error fences will always enclose block
2023-04-23 13:38:28 +01:00
Tom Milligan
0324c93efa fix: ensure error fences will always enclose block 2023-04-23 13:21:51 +01:00
Tom Milligan
62dd36624d Merge pull request #88 from tommilligan/better-fences
fix: better code fence handling
2023-04-23 13:11:09 +01:00
Tom Milligan
b3e82df34e fix: better code fence handling 2023-04-23 12:29:54 +01:00
Tom Milligan
e8813eb104 Merge pull request #84 from ShaunSHamilton/feat_default-title
feat: add default title option
2023-04-20 15:59:05 +01:00
Tom Milligan
f606ad8758 internal: rename structs for clarity 2023-04-20 15:43:57 +01:00
Shaun Hamilton
d269838765 feat: add book-wide default values 2023-04-20 15:43:56 +01:00
Tom Milligan
082359e562 Merge pull request #85 from tommilligan/regression-test-alternate-directive
test: add regression test for alternate directives
2023-04-18 07:58:04 +01:00
Tom Milligan
84d163c32f test: add regression test for alternate directives 2023-04-18 07:34:05 +01:00
Tom Milligan
90484a44ea Merge pull request #83 from ShaunSHamilton/fix_title-height
fix(compile_assets): add title min-height
2023-04-16 12:46:38 +01:00
Shaun Hamilton
5e7674d1f9 fix(compile_assets): add title min-height 2023-04-16 12:33:18 +01:00
Tom Milligan
55a654dfca doc: option for processing included files 2023-04-03 11:52:05 +01:00
Tom Milligan
a2f664cac3 chore: update dependencies, toml errors 2023-04-03 08:35:10 +01:00
Tom Milligan
dfb70b0415 chore: rm cargo-audit 2023-04-02 10:03:34 +01:00
Tom Milligan
fb1b789386 chore: dep updates 2023-01-01 12:36:55 +00:00
Tom Milligan
97bcd97c64 chore: bump deps 2022-12-02 17:27:42 +00:00
Tom Milligan
a2a7316b26 bug: fix relative paths 2022-11-26 22:53:46 +00:00
Tom Milligan
f9eb198cd0 bin: better error handling 2022-11-26 22:36:49 +00:00
dependabot[bot]
5530da074b chore(deps): bump JamesIves/github-pages-deploy-action
Bumps [JamesIves/github-pages-deploy-action](https://github.com/JamesIves/github-pages-deploy-action) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/JamesIves/github-pages-deploy-action/releases)
- [Commits](https://github.com/JamesIves/github-pages-deploy-action/compare/v4.4.0...v4.4.1)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,111 @@
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@v4
- 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@v4
- 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 node toolchain
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "yarn"
cache-dependency-path: compile_assets/yarn.lock
- 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@v4
- 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

155
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,155 @@
# 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:
include:
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
name: aarch64-unknown-linux-musl.tar.gz
- target: x86_64-unknown-linux-gnu
# Deliberately pinned to the same version `mdbook` uses to build
# binaries, so we use the same glibc version
#
# ref: https://github.com/rust-lang/mdBook/pull/1955
os: ubuntu-20.04
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@v4
# 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 | cross
if: endsWith(matrix.target, '-unknown-linux-musl')
uses: taiki-e/install-action@v2
with:
tool: cross
- name: Build | Build
if: ${{ !endsWith(matrix.target, '-unknown-linux-musl') }}
run: cargo build --release --target ${{ matrix.target }}
- name: Build | Build (musl)
if: endsWith(matrix.target, '-unknown-linux-musl')
run: cross build --release --target ${{ matrix.target }}
- 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
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
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@v4
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@v4
- 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
View 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@v4
- 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.3
with:
branch: gh-pages
folder: book/book

View File

@@ -1,4 +1,198 @@
## Changelog
# Changelog
## Unreleased
## 1.13.0
### Changed
- Required styles version is now `^3.0.0` (release `1.13.0`). Run `mdbook-admonish install` to update.
- Internal CSS classnames for directives are now prefixed with `admonish-`, so `warning` is now `admonish-warning`. This avoids a conflict with upstream classnames introduced in `mdbook 0.4.35`. Thanks to [@phoenixr-codes](https://github.com/phoenixr-codes) for the report and fix! ([#139](https://github.com/tommilligan/mdbook-admonish/pull/139))
### Fixed
- Some minor inconsistencies in SCSS (and downstream CSS) styles were fixed by adopting Prettier linting ([#138](https://github.com/tommilligan/mdbook-admonish/pull/138))
## 1.12.1
### Fixed
- Panic when searching for an indent in non-ASCII content. Thanks to [@CoralPink](https://github.com/CoralPink) for the report! ([#128](https://github.com/tommilligan/mdbook-admonish/pull/128))
## 1.12.0
### Added
- Admonitions are now supported when indented inside other elements, such as a list. Thanks to [@mattburgess](https://github.com/mattburgess) for the report! ([#124](https://github.com/tommilligan/mdbook-admonish/pull/124))
## 1.11.1
### Fixed
- Reverted internal dependency upgrades that unintentionally increased MSRV from 1.66.0 in 1.11.0
## 1.11.0 (yanked)
**Note:** This release has been yanked.
It unintentionally increased the MSRV from 1.66.0
### Changed
- `gnu` prebuilt binaries are now built on `ubuntu-20.04` to match `mdbook` binaries. Thanks to [@eitsupi](https://github.com/eitsupi) for the fix! ([#118](https://github.com/tommilligan/mdbook-admonish/pull/118))
### Added
- `aarch64-unknown-linux-musl` prebuilt binary now available ([#119](https://github.com/tommilligan/mdbook-admonish/pull/119))
## 1.10.2
### Fixed
- Fixed `cargo install mdbook-admonish` failing due to an internal dependency mismatch with `mdbook` ([#115](https://github.com/tommilligan/mdbook-admonish/pull/115))
## 1.10.1
### Fixed
- Only restyle `summary` elements generated by `mdbook-admonish`. Thanks to [@ImUrX](https://github.com/ImUrX) for the report and fix! ([#112](https://github.com/tommilligan/mdbook-admonish/pull/112))
## 1.10.0
### Changed
- MSRV (minimum supported rust version) is now 1.66.0 for mdbook v0.4.32 ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
### Added
- Support `mdbook test` running doctests inside admonish blocks. Opt-in to this by setting `renderer.test.action_mode = "strip"` ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
- Log a warning when an invalid admonish block is encountered ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
### Fixed
- Document all `book.toml` configuration options [in the reference](https://tommilligan.github.io/mdbook-admonish/reference.html), some of which were previously undocumened ([#109](https://github.com/tommilligan/mdbook-admonish/pull/109))
## 1.9.0
### Changed
- Styles updated to `^2.0.1`. Run `mdbook-admonish install` to update.
- MSRV (minimum supported rust version) is now 1.64.0 for clap v4 ([#79](https://github.com/tommilligan/mdbook-admonish/pull/79))
- More verbose error messages for invalid TOML configurations ([#79](https://github.com/tommilligan/mdbook-admonish/pull/79))
### Added
- User can set book-wide default for title and collapsible properties ([#84](https://github.com/tommilligan/mdbook-admonish/pull/84)), thanks to [@ShaunSHamilton](https://github.com/ShaunSHamilton)
### Fixed
- Custom installation and CSS directories are now normalized ([#49](https://github.com/tommilligan/mdbook-admonish/pull/49))
- Fix title bars with no text rendering badly ([#83](https://github.com/tommilligan/mdbook-admonish/pull/83)), thanks to [@ShaunSHamilton](https://github.com/ShaunSHamilton)
- Better error message display on crash ([#48](https://github.com/tommilligan/mdbook-admonish/pull/48))
- Better support for commonmark code fence syntax ([#88](https://github.com/tommilligan/mdbook-admonish/pull/88), [#89](https://github.com/tommilligan/mdbook-admonish/pull/89))
## 1.8.0
### 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 (yanked)
**Note:** This release has been yanked.
It unintentionally introduced a serious parsing bug.
### 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

1791
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,13 @@
[package]
name = "mdbook-admonish"
version = "1.2.0"
version = "1.13.0"
edition = "2021"
rust-version = "1.66.0"
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"]
@@ -17,26 +18,41 @@ name = "mdbook-admonish"
path = "src/bin/mdbook-admonish.rs"
required-features = ["cli"]
[profile.release]
strip = true
[lib]
name = "mdbook_admonish"
path = "src/lib.rs"
[dependencies]
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.75"
# Note: clap 4.4 increases MSRV to 1.70.0 (2023-06-01)
# To use MSRV supported dependencies, install using the lockfile with
# `cargo install mdbook-admonish --locked`
clap = { version = "4.3", default_features = false, features = ["std", "derive"], optional = true }
env_logger = { version = "0.10", default_features = false, optional = true }
log = "0.4.20"
mdbook = "0.4.35"
once_cell = "1.18.0"
pulldown-cmark = "0.9.3"
regex = "1.9.6"
semver = "1.0.19"
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
# 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.8.1"
toml_edit = { version = "0.20.1", 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"]

122
README.md
View File

@@ -1,10 +1,9 @@
# mdbook-admonish
[![Latest version](https://img.shields.io/crates/v/mdbook-admonish.svg)](https://crates.io/crates/mdbook-admonish)
[![docs.rs](https://img.shields.io/docsrs/mdbook-admonish)](https://docs.rs/mdbook-admonish)
[![docs.rs](https://img.shields.io/badge/docs-available-brightgreen)](https://tommilligan.github.io/mdbook-admonish/)
A preprocessor for [mdbook](https://github.com/rust-lang-nursery/mdBook) to add [Material Design](https://material.io/design) admonishments,
based on the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) implementation.
A preprocessor for [mdbook](https://github.com/rust-lang-nursery/mdBook) to add [Material Design](https://material.io/design) admonishments, based on the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) implementation.
It turns this:
@@ -16,7 +15,15 @@ A beautifully styled message.
into this:
![Simple Message](simple-message.png)
![Simple Message](img/simple-message.png)
## Examples
Read the documentation [here](https://tommilligan.github.io/mdbook-admonish/), to see the actual examples in action. You can see the source in the [`./book`](./book) subdirectory.
Other projects using mdbook-admonish:
- [The Rhai Book](https://rhai.rs/book/)
## Usage
@@ -28,13 +35,16 @@ My example is the best!
```
````
An custom title can be provided, contained in a double quoted JSON string:
![Best Example](img/best-example.png)
````
```admonish example "Simply the best"
My example is the best!
```
````
See the [reference page](https://tommilligan.github.io/mdbook-admonish/reference.html) for a list of supported admonitions. You'll find:
- `info`
- `warning`
- `danger`
- `example`
and quite a few more!
You can also leave out the admonition type altogether, in which case it will default to `note`:
@@ -44,14 +54,15 @@ 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:
![Plain Note](img/plain-note.png)
- `info`
- `warning`
- `danger`
- `example`
### Additional Options
and quite a few more!
See the [`mdbook-admonish` book](https://tommilligan.github.io/mdbook-admonish/) for additional options, such as:
- Custom titles
- Custom styling
- Collapsible blocks
## Installation
@@ -59,11 +70,16 @@ Install the tool:
```bash
cargo install mdbook-admonish
# If you get compilation/installation errors, try a locked installation
cargo install mdbook-admonish --locked
```
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
@@ -88,6 +104,61 @@ Then, build your book as usual:
mdbook path/to/book
```
### Reproducible builds
For a reproducible build suitable for use in CI or scripts, please:
- Pin to a specific version
- Install with lockfile dependencies
- Always install the latest CSS assets
```bash
cargo install mdbook-admonish --vers "1.5.0" --locked
mdbook-admonish install path/to/your/book
```
The Minimum Supported Rust Version (MSRV) is documented in `Cargo.toml`, and noted in the `CHANGELOG.md`. We aims to support around six months of stable Rust.
### 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`.
### 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
@@ -96,6 +167,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
View File

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

21
book/book.toml Normal file
View 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 = "3.0.0" # do not edit: managed by `mdbook-admonish install`
[preprocessor.toc]
command = "mdbook-toc"
renderer = ["html"]
[output]
[output.html]
additional-css = ["./mdbook-admonish.css"]

View File

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

4
book/src/SUMMARY.md Normal file
View File

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

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

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

@@ -0,0 +1,6 @@
# Notes for a v2 release
## Default behaviour changes
- `on_failure` default changed from `continue` to `bail`
- `preprocessor.admonish.renderer.test.render_mode` default changed from `preserve` to `strip`

View File

@@ -4,9 +4,14 @@
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "sass --no-source-map scss/mdbook-admonish.scss ../src/bin/assets/mdbook-admonish.css"
"build": "sass --no-source-map scss/mdbook-admonish.scss ../src/bin/assets/mdbook-admonish.css",
"lint": "prettier --check .",
"fix": "prettier --write ."
},
"dependencies": {
"sass": "^1.49.7"
},
"devDependencies": {
"prettier": "^3.0.3"
}
}

View File

@@ -30,29 +30,41 @@
/// Admonition flavours
$admonitions: (
// pencil
note: $clr-blue-a200 "<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>",
admonish-note: $clr-blue-a200
"<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>",
// clipboard-text
abstract summary tldr: $clr-light-blue-a400 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>",
admonish-abstract admonish-summary admonish-tldr: $clr-light-blue-a400
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>",
// information
info todo: $clr-cyan-a700 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>",
admonish-info admonish-todo: $clr-cyan-a700
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>",
// fire
tip hint important: $clr-teal-a700 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>",
admonish-tip admonish-hint admonish-important: $clr-teal-a700
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>",
// check-bold
success check done: $clr-green-a700 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>",
admonish-success admonish-check admonish-done: $clr-green-a700
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>",
// help-circle
question help faq: $clr-light-green-a700 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>",
admonish-question admonish-help admonish-faq: $clr-light-green-a700
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>",
// alert
warning caution attention: $clr-orange-a400 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>",
admonish-warning admonish-caution admonish-attention: $clr-orange-a400
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>",
// close-thick
failure fail missing: $clr-red-a200 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>",
admonish-failure admonish-fail admonish-missing: $clr-red-a200
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>",
// lighting-bold
danger error: $clr-red-a400 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>",
admonish-danger admonish-error: $clr-red-a400
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>",
// bug
bug: $clr-pink-a400 "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>",
admonish-bug: $clr-pink-a400
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>",
// format-list-numbered
example: $clr-deep-purple-a200 "<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>",
admonish-example: $clr-deep-purple-a200
"<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>",
// format-quote-close
quote cite: $clr-grey "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>",
admonish-quote admonish-cite: $clr-grey
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>"
) !default;
// ----------------------------------------------------------------------------
@@ -62,14 +74,13 @@ $admonitions: (
// Admonition variables
:root {
@each $names, $props in $admonitions {
--md-admonition-icon--#{nth($names, 1)}:
url("data:image/svg+xml;charset=utf-8,#{nth($props, 2)}");
--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>");
}
// ----------------------------------------------------------------------------
// Admonition
:is(.admonition) {
display: flow-root;
@@ -84,7 +95,9 @@ $admonitions: (
border: 0 solid black;
border-inline-start-width: 0.4rem;
border-radius: 0.2rem;
box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.05), 0 0 0.1rem rgba(0, 0, 0, 0.1);;
box-shadow:
0 0.2rem 1rem rgba(0, 0, 0, 0.05),
0 0 0.1rem rgba(0, 0, 0, 0.1);
// [print]: Omit shadow as it may lead to rendering errors
@media print {
@@ -114,18 +127,51 @@ $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: 1rem;
&: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 +182,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 +194,35 @@ $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: 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 > &::after {
transform: rotate(90deg);
}
}
// ----------------------------------------------------------------------------
@@ -160,6 +235,7 @@ $admonitions: (
// Admonition flavour selectors
$flavours: ();
@each $name in $names {
$flavours: list.join($flavours, ".#{$name}", $separator: comma);
}
@@ -170,9 +246,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 {
@@ -199,7 +274,8 @@ $admonitions: (
}
}
.ayu, .coal {
.ayu,
.coal {
& :is(.admonition) {
background-color: var(--theme-hover);
}
@@ -210,4 +286,11 @@ $admonitions: (
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
& .admonition-anchor-link {
&:link,
&:visited {
color: var(--sidebar-fg);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -95,6 +95,11 @@ picomatch@^2.0.4, picomatch@^2.2.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
prettier@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"

BIN
img/best-example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
img/code-bug.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
img/complex-message.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
img/data-loss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
img/no-title-bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
img/plain-note.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

2
integration/.gitignore vendored Normal file
View File

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

21
integration/book.toml Normal file
View 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 = "3.0.0" # 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"]

View 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 = "3.0.0" # 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"]

View File

@@ -0,0 +1,118 @@
<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>
<div id="admonition-what-is-this" class="admonition admonish-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 admonish-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 admonish-warning">
<div>
<p>No title, only body</p>
</div>
</div>
<div id="admonition-error-rendering-admonishment" class="admonition admonish-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=&quot;
| ^
invalid basic string
</code></pre>
<p>Original markdown input:</p>
<pre><code class="language-markdown">```admonish title=&quot;
No title, only body
```
</code></pre>
</div>
</div>
<details id="admonition-note-1" class="admonition admonish-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 admonish-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 admonish-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 admonish-note">
<div class="admonition-title">
<p>Note</p>
<p><a class="admonition-anchor-link" href="#admonition-note-3"></a></p>
</div>
<div>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let x = 10;
x = 20;
<span class="boring">}</span></code></pre></pre>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let x = 10;
let x = 20;
<span class="boring">}</span></code></pre></pre>
</div>
</div>
<p>In a list:</p>
<ol>
<li>
<p>Thing one</p>
<pre><code class="language-sh">Thing one
</code></pre>
</li>
<li>
<p>Thing two</p>
<div id="admonition-note-4" class="admonition admonish-note">
<div class="admonition-title">
<p>Note</p>
<p><a class="admonition-anchor-link" href="#admonition-note-4"></a></p>
</div>
<div>
<p>Thing two</p>
</div>
</div>
</li>
<li>
<p>Thing three</p>
<pre><code class="language-sh">Thing three
</code></pre>
</li>
</ol>

71
integration/scripts/check Executable file
View 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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,63 @@
# 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;
```
````
In a list:
1. Thing one
```sh
Thing one
```
1. Thing two
```admonish
Thing two
```
1. Thing three
```sh
Thing three
```

View File

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

14
qvet.yml Normal file
View 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
View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Build book, using styles present in the repository.
set -euo pipefail
cd "$(dirname "$0")"/..
function eprintln() {
>&2 echo "$1"
}
eprintln "Installing mdbook-admonish (to system)"
cargo install --path . --force
pushd book
eprintln "Installing mdbook-admonish (to book)"
mdbook-admonish install .
eprintln "Building book"
mdbook build
popd
eprintln "Book generated at ./book/book/index.html"

44
scripts/check Executable file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"/..
function eprintln() {
>&2 echo "$1"
}
# Node things
pushd compile_assets > /dev/null
eprintln "Linting style sources"
yarn run lint
eprintln "Checking compiled styles up to date"
COMITTED_ASSETS="$(cat ../src/bin/assets/mdbook-admonish.css)"
yarn run build
RECOMPILED_ASSETS="$(cat ../src/bin/assets/mdbook-admonish.css)"
diff -u <(printf "%s" "$COMITTED_ASSETS") <(printf "%s" "$RECOMPILED_ASSETS")
popd > /dev/null
# Rust things
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
# Integration test
eprintln "Running mdbook integration test"
./integration/scripts/check

24
scripts/install Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Install everything for development/CI
#
# Does not include offline node development stack (i.e. yarn)
set -euo pipefail
function eprintln() {
>&2 echo "$1"
}
cd "$(dirname "$0")"/..
eprintln "Installing additional Rust components"
rustup component add rustfmt clippy
eprintln "Installing mdbook"
./scripts/install-mdbook
eprintln "Installing node dependencies"
pushd compile_assets > /dev/null
yarn install --frozen-lockfile
popd > /dev/null

18
scripts/install-mdbook Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"/..
function eprintln() {
>&2 echo "$1"
}
VERSION="0.4.35"
eprintln "Checking if mdbook $VERSION is installed"
if [[ "$(mdbook --version)" != "mdbook v$VERSION" ]]; then
eprintln "Installing mdbook $VERSION"
cargo install mdbook --version "$VERSION" --force
fi
eprintln "mdbook $VERSION is installed"

13
scripts/publish Executable file
View File

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

18
scripts/rebuild-book Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Development only. Rebuilds the book, including recompiling styles.
set -euo pipefail
cd "$(dirname "$0")"/..
function eprintln() {
>&2 echo "$1"
}
eprintln "Generating styles"
pushd compile_assets
yarn run build
popd
./scripts/build-book

View File

@@ -0,0 +1 @@
^3.0.0

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

@@ -0,0 +1 @@
3.0.0

View File

@@ -1,28 +1,18 @@
@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>");
--md-admonition-icon--abstract:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
--md-admonition-icon--info:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
--md-admonition-icon--tip:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
--md-admonition-icon--success:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
--md-admonition-icon--question:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
--md-admonition-icon--warning:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
--md-admonition-icon--failure:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
--md-admonition-icon--danger:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
--md-admonition-icon--bug:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
--md-admonition-icon--example:
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-admonition-icon--admonish-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>");
--md-admonition-icon--admonish-abstract: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
--md-admonition-icon--admonish-info: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
--md-admonition-icon--admonish-tip: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
--md-admonition-icon--admonish-success: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
--md-admonition-icon--admonish-question: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
--md-admonition-icon--admonish-warning: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
--md-admonition-icon--admonish-failure: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
--md-admonition-icon--admonish-danger: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
--md-admonition-icon--admonish-bug: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
--md-admonition-icon--admonish-example: 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--admonish-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 +46,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,217 +94,229 @@ 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;
}
:is(.admonition):is(.note) {
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(.admonish-note) {
border-color: #448aff;
}
:is(.note) > :is(.admonition-title, summary) {
:is(.admonish-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(.admonish-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);
mask-image: var(--md-admonition-icon--admonish-note);
-webkit-mask-image: var(--md-admonition-icon--admonish-note);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.abstract, .summary, .tldr) {
:is(.admonition):is(.admonish-abstract, .admonish-summary, .admonish-tldr) {
border-color: #00b0ff;
}
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary) {
:is(.admonish-abstract, .admonish-summary, .admonish-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(.admonish-abstract, .admonish-summary, .admonish-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);
mask-image: var(--md-admonition-icon--admonish-abstract);
-webkit-mask-image: var(--md-admonition-icon--admonish-abstract);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.info, .todo) {
:is(.admonition):is(.admonish-info, .admonish-todo) {
border-color: #00b8d4;
}
:is(.info, .todo) > :is(.admonition-title, summary) {
:is(.admonish-info, .admonish-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(.admonish-info, .admonish-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);
mask-image: var(--md-admonition-icon--admonish-info);
-webkit-mask-image: var(--md-admonition-icon--admonish-info);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.tip, .hint, .important) {
:is(.admonition):is(.admonish-tip, .admonish-hint, .admonish-important) {
border-color: #00bfa5;
}
:is(.tip, .hint, .important) > :is(.admonition-title, summary) {
:is(.admonish-tip, .admonish-hint, .admonish-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(.admonish-tip, .admonish-hint, .admonish-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);
mask-image: var(--md-admonition-icon--admonish-tip);
-webkit-mask-image: var(--md-admonition-icon--admonish-tip);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.success, .check, .done) {
:is(.admonition):is(.admonish-success, .admonish-check, .admonish-done) {
border-color: #00c853;
}
:is(.success, .check, .done) > :is(.admonition-title, summary) {
:is(.admonish-success, .admonish-check, .admonish-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(.admonish-success, .admonish-check, .admonish-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);
mask-image: var(--md-admonition-icon--admonish-success);
-webkit-mask-image: var(--md-admonition-icon--admonish-success);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.question, .help, .faq) {
:is(.admonition):is(.admonish-question, .admonish-help, .admonish-faq) {
border-color: #64dd17;
}
:is(.question, .help, .faq) > :is(.admonition-title, summary) {
:is(.admonish-question, .admonish-help, .admonish-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(.admonish-question, .admonish-help, .admonish-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);
mask-image: var(--md-admonition-icon--admonish-question);
-webkit-mask-image: var(--md-admonition-icon--admonish-question);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.warning, .caution, .attention) {
:is(.admonition):is(.admonish-warning, .admonish-caution, .admonish-attention) {
border-color: #ff9100;
}
:is(.warning, .caution, .attention) > :is(.admonition-title, summary) {
:is(.admonish-warning, .admonish-caution, .admonish-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(.admonish-warning, .admonish-caution, .admonish-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);
mask-image: var(--md-admonition-icon--admonish-warning);
-webkit-mask-image: var(--md-admonition-icon--admonish-warning);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.failure, .fail, .missing) {
:is(.admonition):is(.admonish-failure, .admonish-fail, .admonish-missing) {
border-color: #ff5252;
}
:is(.failure, .fail, .missing) > :is(.admonition-title, summary) {
:is(.admonish-failure, .admonish-fail, .admonish-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(.admonish-failure, .admonish-fail, .admonish-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);
mask-image: var(--md-admonition-icon--admonish-failure);
-webkit-mask-image: var(--md-admonition-icon--admonish-failure);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.danger, .error) {
:is(.admonition):is(.admonish-danger, .admonish-error) {
border-color: #ff1744;
}
:is(.danger, .error) > :is(.admonition-title, summary) {
:is(.admonish-danger, .admonish-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(.admonish-danger, .admonish-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);
mask-image: var(--md-admonition-icon--admonish-danger);
-webkit-mask-image: var(--md-admonition-icon--admonish-danger);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.bug) {
:is(.admonition):is(.admonish-bug) {
border-color: #f50057;
}
:is(.bug) > :is(.admonition-title, summary) {
:is(.admonish-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(.admonish-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);
mask-image: var(--md-admonition-icon--admonish-bug);
-webkit-mask-image: var(--md-admonition-icon--admonish-bug);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.example) {
:is(.admonition):is(.admonish-example) {
border-color: #7c4dff;
}
:is(.example) > :is(.admonition-title, summary) {
:is(.admonish-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(.admonish-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);
mask-image: var(--md-admonition-icon--admonish-example);
-webkit-mask-image: var(--md-admonition-icon--admonish-example);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.quote, .cite) {
:is(.admonition):is(.admonish-quote, .admonish-cite) {
border-color: #9e9e9e;
}
:is(.quote, .cite) > :is(.admonition-title, summary) {
:is(.admonish-quote, .admonish-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(.admonish-quote, .admonish-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);
mask-image: var(--md-admonition-icon--admonish-quote);
-webkit-mask-image: var(--md-admonition-icon--admonish-quote);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
@@ -307,7 +327,8 @@ html :is(.admonition-title, summary):last-child {
background-color: var(--sidebar-bg);
}
.ayu :is(.admonition), .coal :is(.admonition) {
.ayu :is(.admonition),
.coal :is(.admonition) {
background-color: var(--theme-hover);
}
@@ -315,3 +336,6 @@ html :is(.admonition-title, summary):last-child {
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.rust .admonition-anchor-link:link, .rust .admonition-anchor-link:visited {
color: var(--sidebar-fg);
}

View File

@@ -1,57 +1,70 @@
use clap::{crate_version, Arg, ArgMatches, Command};
use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
use anyhow::Result;
use clap::{Parser, Subcommand};
use mdbook::{
errors::Error,
preprocess::{CmdPreprocessor, Preprocessor},
};
use mdbook_admonish::Admonish;
#[cfg(feature = "cli-install")]
use std::path::PathBuf;
use std::{io, process};
pub fn make_app() -> Command<'static> {
let mut command = Command::new("mdbook-admonish")
.version(crate_version!())
.about("mdbook preprocessor to add support for admonitions");
command = command.subcommand(
Command::new("supports")
.arg(Arg::new("renderer").required(true))
.about("Check whether a renderer is supported by this preprocessor"),
);
/// mdbook preprocessor to add support for admonitions
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
/// Check whether a renderer is supported by this preprocessor
Supports { renderer: String },
#[cfg(feature = "cli-install")]
{
command = command.subcommand(
Command::new("install")
.arg(Arg::new("css-dir").long("css-dir").default_value(".").help(
"Relative directory for the css assets,\nfrom the book directory root",
))
.arg(Arg::new("dir").default_value(".").help(
"Root directory for the book,\nshould contain the configuration file (`book.toml`)",
))
.about("Install the required assset files and include it in the config"));
}
command
/// Install the required assset files and include it in the config
Install {
/// Root directory for the book, should contain the configuration file (`book.toml`)
///
/// If not set, defaults to the current directory.
dir: Option<PathBuf>,
/// Relative directory for the css assets, from the book directory root
///
/// If not set, defaults to the current directory.
#[arg(long)]
css_dir: Option<PathBuf>,
},
}
fn main() {
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
let matches = make_app().get_matches();
if let Some(sub_args) = matches.subcommand_matches("supports") {
handle_supports(sub_args);
} else if let Some(sub_args) = matches.subcommand_matches("install") {
#[cfg(feature = "cli-install")]
{
install::handle_install(sub_args);
let cli = Cli::parse();
if let Err(error) = run(cli) {
log::error!("Fatal error: {}", error);
for error in error.chain() {
log::error!(" - {}", error);
}
#[cfg(not(feature = "cli-install"))]
{
panic!("cli-install feature not enabled: {:?}", sub_args)
}
} else if let Err(e) = handle_preprocessing() {
eprintln!("{}", e);
process::exit(1);
}
}
fn run(cli: Cli) -> Result<()> {
match cli.command {
None => handle_preprocessing(),
Some(Commands::Supports { renderer }) => {
handle_supports(renderer);
}
#[cfg(feature = "cli-install")]
Some(Commands::Install { dir, css_dir }) => install::handle_install(
dir.unwrap_or_else(|| PathBuf::from(".")),
css_dir.unwrap_or_else(|| PathBuf::from(".")),
),
}
}
fn handle_preprocessing() -> Result<(), Error> {
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
@@ -70,9 +83,8 @@ fn handle_preprocessing() -> Result<(), Error> {
Ok(())
}
fn handle_supports(sub_args: &ArgMatches) -> ! {
let renderer = sub_args.value_of("renderer").expect("Required argument");
let supported = Admonish.supports_renderer(renderer);
fn handle_supports(renderer: String) -> ! {
let supported = Admonish.supports_renderer(&renderer);
// Signal whether the renderer is supported by exiting with 1 or 0.
if supported {
@@ -84,12 +96,11 @@ fn handle_supports(sub_args: &ArgMatches) -> ! {
#[cfg(feature = "cli-install")]
mod install {
use clap::ArgMatches;
use anyhow::{Context, Result};
use std::{
fs::{self, File},
io::Write,
path::PathBuf,
process,
};
use toml_edit::{self, Array, Document, Item, Table, Value};
@@ -111,31 +122,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
View File

@@ -0,0 +1,60 @@
use anyhow::{Context, Result};
use mdbook::preprocess::PreprocessorContext;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::types::AdmonitionDefaults;
/// Loads the plugin configuration from mdbook internals.
///
/// Roundtrips config to string, to avoid linking the plugin's internal version of toml
/// to the one publically exposed by the mdbook library.
pub(crate) fn admonish_config_from_context(ctx: &PreprocessorContext) -> Result<Config> {
let table: String = toml_mdbook::to_string(
ctx.config
.get_preprocessor("admonish")
.context("No configuration for mdbook-admonish in book.toml")?,
)?;
toml::from_str(&table).context("Invalid mdbook-admonish configuration in book.toml")
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct Config {
#[serde(default)]
pub on_failure: OnFailure,
#[serde(default)]
pub default: AdmonitionDefaults,
#[serde(default)]
pub renderer: HashMap<String, RendererConfig>,
#[serde(default)]
pub assets_version: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct RendererConfig {
pub render_mode: Option<RenderMode>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum RenderMode {
Preserve,
Strip,
Html,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum OnFailure {
Bail,
Continue,
}
impl Default for OnFailure {
fn default() -> Self {
Self::Continue
}
}

89
src/config/mod.rs Normal file
View 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
View 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
View 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 `.`, `=`
"#
);
}
}

View File

@@ -1,453 +1,10 @@
use mdbook::book::{Book, BookItem, Chapter};
use mdbook::errors::Result as MdbookResult;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag};
use std::borrow::Cow;
use std::str::FromStr;
pub struct Admonish;
impl Preprocessor for Admonish {
fn name(&self) -> &str {
"admonish"
}
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> MdbookResult<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("&lt;"),
'>' => output.push_str("&gt;"),
'"' => output.push_str("&quot;"),
'&' => output.push_str("&amp;"),
_ => 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) -> 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(()),
}
}
}
impl Directive {
fn classname(&self) -> &'static str {
match self {
Directive::Note => "note",
Directive::Abstract => "abstract",
Directive::Info => "info",
Directive::Tip => "tip",
Directive::Success => "success",
Directive::Question => "question",
Directive::Warning => "warning",
Directive::Failure => "failure",
Directive::Danger => "danger",
Directive::Bug => "bug",
Directive::Example => "example",
Directive::Quote => "quote",
}
}
}
#[derive(Debug, PartialEq)]
struct AdmonitionInfo<'a> {
directive: &'a str,
title: Option<&'a str>,
}
#[derive(Debug, PartialEq)]
struct Admonition<'a> {
directive: Directive,
title: Cow<'a, str>,
}
impl<'a> Default for Admonition<'a> {
fn default() -> Self {
Self {
directive: Directive::Note,
title: Cow::Borrowed("Note"),
}
}
}
impl<'a> TryFrom<AdmonitionInfo<'a>> for Admonition<'a> {
type Error = ();
fn try_from(other: AdmonitionInfo<'a>) -> Result<Self, ()> {
let directive = Directive::from_str(other.directive)?;
Ok(Self {
directive,
title: other
.title
.map(Cow::Borrowed)
.unwrap_or_else(|| Cow::Owned(ucfirst(other.directive))),
})
}
}
/// Returns:
/// - `None` if this is not an `admonish` block.
/// - `Some(None)` if this is an `admonish` block, but no further configuration was given
/// - `Some(AdmonitionInfo)` if this is an `admonish` block, and further configuration was given
fn parse_info_string(info_string: &str) -> Option<Option<AdmonitionInfo>> {
if info_string == "admonish" {
return Some(None);
}
let directive_title = match info_string.split_once(' ') {
Some(("admonish", rest)) => rest,
_ => return None,
};
let info = if let Some((directive, title)) = directive_title.split_once(' ') {
// The title is expected to be a quoted JSON string
let title = serde_json::from_str(title).ok();
AdmonitionInfo { directive, title }
} else {
AdmonitionInfo {
directive: directive_title,
title: None,
}
};
Some(Some(info))
}
/// Make the first letter of `input` upppercase.
///
/// source: https://stackoverflow.com/a/38406885
fn ucfirst(input: &str) -> String {
let mut chars = input.chars();
match chars.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
}
}
fn add_admonish(content: &str) -> MdbookResult<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 info = match parse_info_string(info_string.as_ref()) {
Some(info) => info,
None => continue,
};
let admonition = info
.map(|info| Admonition::try_from(info).unwrap_or_default())
.unwrap_or_default();
const PRE_START: &str = "```";
const PRE_END: &str = "\n";
const POST: &str = "```";
let start_index = span.start + PRE_START.len() + info_string.len() + PRE_END.len();
let end_index = span.end - POST.len();
let admonish_content = &content[start_index..end_index];
let admonish_content = escape_html(admonish_content);
let admonish_content = admonish_content.trim();
// Note that the additional whitespace around the content are deliberate
// In line with the commonmark spec, this allows the inner content to be
// rendered as markdown again.
let admonish_code = format!(
r#"<div class="admonition {directive_classname}">
<p class="admonition-title">{directive_title}</p>
<p>
{admonish_content}
</p>
</div>"#,
directive_classname = admonition.directive.classname(),
directive_title = admonition.title,
);
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) -> MdbookResult<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(AdmonitionInfo {
directive: "",
title: None,
}))
);
assert_eq!(
parse_info_string("admonish unknown"),
Some(Some(AdmonitionInfo {
directive: "unknown",
title: None
}))
);
assert_eq!(
parse_info_string("admonish note"),
Some(Some(AdmonitionInfo {
directive: "note",
title: None
}))
);
}
#[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 adds_admonish_directive_title() {
let content = r#"# Chapter
```admonish warning "Read **this**!"
A simple admonition.
```
Text
"#;
let expected = r#"# Chapter
<div class="admonition warning">
<p class="admonition-title">Read **this**!</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 {
&lt;&lt;interface&gt;&gt;
+Upload() UploadResult
}
</p>
</div>
hello
"#;
assert_eq!(expected, add_admonish(content).unwrap());
}
}
mod book_config;
mod config;
mod markdown;
mod parse;
mod preprocessor;
mod render;
mod resolve;
mod types;
pub use crate::preprocessor::Admonish;

858
src/markdown.rs Normal file
View File

@@ -0,0 +1,858 @@
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];
const INDENT_SCAN_MAX: usize = 1024;
let indent = indent_of(content, span.start, INDENT_SCAN_MAX);
let admonition = match parse_admonition(
info_string.as_ref(),
admonition_defaults,
span_content,
on_failure,
indent,
) {
Some(admonition) => admonition,
None => continue,
};
let admonition = admonition?;
// Once we've identitified admonition blocks, handle them differently
// depending on our render mode
let new_content = match render_text_mode {
RenderTextMode::Html => admonition.html_with_unique_ids(&mut id_counter),
RenderTextMode::Strip => admonition.strip(),
};
admonish_blocks.push((span, new_content));
}
}
let mut content = content.to_string();
for (span, block) in admonish_blocks.iter().rev() {
let pre_content = &content[..span.start];
let post_content = &content[span.end..];
content = format!("{}{}{}", pre_content, block, post_content);
}
Ok(content)
}
/// Returns the indent of the given position.
///
/// Defined as the number of characters between the given `position` (where
/// position is a valid char boundary byte-index in `content`),
/// and the previous newline character `\n`.
///
/// `max` is the maximum number of characters to scan before assuming there is
/// no indent (will return zero if exceeded).
///
/// ## Panics
///
/// Will panic if `position` is not a valid utf-8 char boundary index of `content`.
fn indent_of(content: &str, position: usize, max: usize) -> usize {
// Scan for a line start before this span.
content[..position]
.chars()
.rev()
// For safety, only scan up to a fixed limit of the text
.take(max)
.position(|c| c == '\n')
// If we can't find a newline, assume no indent
.unwrap_or_default()
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn indent_of_samples() {
for (content, position, max, expected) in [
// Empty case
("", 0, 10, 0),
("no newline", 4, 10, 0),
// Newline at position 5, difference from 8 is 3
("with\nnewline", 8, 10, 3),
// If no newline in safety limit, return 0
("with\nnewline", 8, 2, 0),
// Safety limit is characters, not bytes
// Regression test for FIXME LINK
(
"例えばこれは",
// Position is generated by mdbook internals, should be valid char limit
// This mimics the second character starting the span
"".len(),
// Any arbitrary safetly limit should be valid
1,
// Should not panic
0,
),
(
"例え\n れは",
// Position is generated by mdbook internals, should be valid char limit
// This mimics the second character starting the span
"例え\n ".len(),
// Any arbitrary safetly limit should be valid
4,
// Should not panic
2,
),
] {
let actual = indent_of(content, position, max);
assert_eq!(actual, expected);
}
}
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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-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 admonish-note">
<div>
A simple admonition.
</div>
</div>
Text
"#;
assert_eq!(expected, prep(content));
}
#[test]
fn list_embed() {
let content = r#"# Chapter
1. Thing one
```sh
Thing one
```
1. Thing two
```admonish
Thing two
```
1. Thing three
```sh
Thing three
```
"#;
let expected = r##"# Chapter
1. Thing one
```sh
Thing one
```
1. Thing two
<div id="admonition-note" class="admonition admonish-note">
<div class="admonition-title">
Note
<a class="admonition-anchor-link" href="#admonition-note"></a>
</div>
<div>
Thing two
</div>
</div>
1. Thing three
```sh
Thing three
```
"##;
assert_eq!(expected, prep(content));
}
}

240
src/parse.rs Normal file
View File

@@ -0,0 +1,240 @@
use anyhow::{anyhow, Result};
use std::borrow::Cow;
pub use crate::preprocessor::Admonish;
use crate::{
book_config::OnFailure,
render::Admonition,
resolve::AdmonitionMeta,
types::{AdmonitionDefaults, Directive},
};
/// Given the content in the span of the code block, and the info string,
/// return `Some(Admonition)` if the code block is an admonition.
///
/// If there is an error parsing the admonition, either:
///
/// - Display a UI error message output in the book.
/// - If configured, break the build.
///
/// If the code block is not an admonition, return `None`.
pub(crate) fn parse_admonition<'a>(
info_string: &'a str,
admonition_defaults: &'a AdmonitionDefaults,
content: &'a str,
on_failure: OnFailure,
indent: usize,
) -> Option<Result<Admonition<'a>>> {
// We need to know fence details anyway for error messages
let extracted = extract_admonish_body(content);
let info = AdmonitionMeta::from_info_string(info_string, admonition_defaults)?;
let info = match info {
Ok(info) => info,
Err(message) => {
// Construct a fence capable of enclosing whatever we wrote for the
// actual input block
let fence = extracted.fence;
let enclosing_fence: String = std::iter::repeat(fence.character)
.take(fence.length + 1)
.collect();
return Some(match on_failure {
OnFailure::Continue => {
log::warn!(
r#"Error processing admonition. To fail the build instead of continuing, set 'on_failure = "bail"'"#
);
Ok(Admonition {
directive: Directive::Bug,
title: "Error rendering admonishment".to_owned(),
additional_classnames: Vec::new(),
collapsible: false,
content: Cow::Owned(format!(
r#"Failed with:
```log
{message}
```
Original markdown input:
{enclosing_fence}markdown
{content}
{enclosing_fence}
"#
)),
indent,
})
}
OnFailure::Bail => Err(anyhow!("Error processing admonition, bailing:\n{content}")),
});
}
};
Some(Ok(Admonition::new(
info,
extracted.body,
// Note that this is a bit hacky - the fence information comes from the start
// of the block, and includes the whole line.
//
// This is more likely to be what we want, as ending indentation is unrelated
// according to the commonmark spec (ref https://spec.commonmark.org/0.12/#example-85)
//
// The main case we're worried about here is indenting enough to be inside list items,
// and in this case the starting code fence must be indented enough to be considered
// part of the list item.
//
// The hacky thing is that we're considering line indent in the document as a whole,
// not relative to the context of some containing item. But I think that's what we
// want for now, anyway.
indent,
)))
}
/// We can't trust the info string length to find the start of the body
/// it may change length if it contains HTML or character escapes.
///
/// So we scan for the first newline and use that.
/// If gods forbid it doesn't exist for some reason, just include the whole info string.
fn extract_admonish_body_start_index(content: &str) -> usize {
let index = content
.find('\n')
// Start one character _after_ the newline
.map(|index| index + 1);
// If we can't get a valid index, include all content
match index {
// Couldn't find a newline
None => 0,
Some(index) => {
// Index out of bound of content
if index > (content.len() - 1) {
0
} else {
index
}
}
}
}
fn extract_admonish_body_end_index(content: &str) -> (usize, Fence) {
let fence_character = content.chars().next_back().unwrap_or('`');
let number_fence_characters = content
.chars()
.rev()
.position(|c| c != fence_character)
.unwrap_or_default();
let fence = Fence::new(fence_character, number_fence_characters);
let index = content.len() - fence.length;
(index, fence)
}
#[derive(Debug, PartialEq)]
struct Fence {
character: char,
length: usize,
}
impl Fence {
fn new(character: char, length: usize) -> Self {
Self { character, length }
}
}
#[derive(Debug, PartialEq)]
struct Extracted<'a> {
body: &'a str,
fence: Fence,
}
/// Given the whole text content of the code fence, extract the body.
///
/// This really feels like we should get the markdown parser to do it for us,
/// but it's not really clear a good way of doing that.
///
/// ref: https://spec.commonmark.org/0.30/#fenced-code-blocks
fn extract_admonish_body(content: &str) -> Extracted<'_> {
let start_index = extract_admonish_body_start_index(content);
let (end_index, fence) = extract_admonish_body_end_index(content);
let admonish_content = &content[start_index..end_index];
// The newline after a code block is technically optional, so we have to
// trim it off dynamically.
let body = admonish_content.trim_end();
Extracted { body, fence }
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_extract_start() {
for (text, expected) in [
("```sane example\ncontent```", 16),
("~~~~~\nlonger fence", 6),
// empty
("```\n```", 4),
// bounds check, should not index outside of content
("```\n", 0),
] {
let actual = extract_admonish_body_start_index(text);
assert_eq!(actual, expected);
}
}
#[test]
fn test_extract_end() {
for (text, expected) in [
("\n```", (1, Fence::new('`', 3))),
// different lengths
("\n``````", (1, Fence::new('`', 6))),
("\n~~~~", (1, Fence::new('~', 4))),
// whitespace before fence end
("\n ```", (4, Fence::new('`', 3))),
("content\n```", (8, Fence::new('`', 3))),
] {
let actual = extract_admonish_body_end_index(text);
assert_eq!(actual, expected);
}
}
#[test]
fn test_extract() {
fn content_fence(body: &'static str, character: char, length: usize) -> Extracted<'static> {
Extracted {
body,
fence: Fence::new(character, length),
}
}
for (text, expected) in [
// empty
("```\n```", content_fence("", '`', 3)),
// standard
(
"```admonish\ncontent\n```",
content_fence("content", '`', 3),
),
// whitespace
(
"```admonish \n content \n ```",
content_fence(" content", '`', 3),
),
// longer
(
"``````admonish\ncontent\n``````",
content_fence("content", '`', 6),
),
// unequal
(
"~~~admonish\ncontent\n~~~~~",
// longer (end) fence returned
content_fence("content", '~', 5),
),
] {
let actual = extract_admonish_body(text);
assert_eq!(actual, expected);
}
}
}

259
src/preprocessor.rs Normal file
View File

@@ -0,0 +1,259 @@
use anyhow::{anyhow, Result};
use mdbook::{
book::{Book, BookItem},
errors::Result as MdbookResult,
preprocess::{Preprocessor, PreprocessorContext},
};
use crate::{
book_config::{admonish_config_from_context, Config, RenderMode},
markdown::preprocess,
types::RenderTextMode,
};
pub struct Admonish;
impl Preprocessor for Admonish {
fn name(&self) -> &str {
"admonish"
}
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> MdbookResult<Book> {
let config = admonish_config_from_context(ctx)?;
ensure_compatible_assets_version(&config)?;
let on_failure = config.on_failure;
let admonition_defaults = config.default;
// Load what rendering we should do from config, falling back to a default
let render_mode = config
.renderer
.get(&ctx.renderer)
.and_then(|renderer| renderer.render_mode)
.unwrap_or_else(|| {
// By default only render html for the html renderer
// For everything else, do nothing
if &ctx.renderer == "html" {
RenderMode::Html
} else {
RenderMode::Preserve
}
});
let render_text_mode = match render_mode {
RenderMode::Preserve => return Ok(book),
RenderMode::Html => RenderTextMode::Html,
RenderMode::Strip => RenderTextMode::Strip,
};
let mut res = None;
book.for_each_mut(|item: &mut BookItem| {
if let Some(Err(_)) = res {
return;
}
if let BookItem::Chapter(ref mut chapter) = *item {
res = Some(
preprocess(
&chapter.content,
on_failure,
&admonition_defaults,
render_text_mode,
)
.map(|md| {
chapter.content = md;
}),
);
}
});
res.unwrap_or(Ok(())).map(|_| book)
}
fn supports_renderer(&self, _renderer: &str) -> bool {
// We support all renderers, but will only actually take action
// if configured to do so - or, if it's the html renderer
true
}
}
fn ensure_compatible_assets_version(config: &Config) -> Result<()> {
use semver::{Version, VersionReq};
const REQUIRES_ASSETS_VERSION: &str = std::include_str!("./REQUIRED_ASSETS_VERSION");
let requirement = VersionReq::parse(REQUIRES_ASSETS_VERSION.trim()).unwrap();
const USER_ACTION: &str = "Please run `mdbook-admonish install` to update installed assets.";
const DOCS_REFERENCE: &str = "For more information, see: https://github.com/tommilligan/mdbook-admonish#semantic-versioning";
let version = match &config.assets_version {
Some(version) => version,
None => {
return Err(anyhow!(
r#"ERROR:
Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but did not find a version.
{USER_ACTION}
{DOCS_REFERENCE}"#
))
}
};
let version = Version::parse(version).unwrap();
if !requirement.matches(&version) {
return Err(anyhow!(
r#"ERROR:
Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but found '{version}'.
{USER_ACTION}
{DOCS_REFERENCE}"#
));
};
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::{json, Value};
fn mock_book(content: &str) -> Book {
serde_json::from_value(json!({
"sections": [
{
"Chapter": {
"name": "Chapter 1",
"content": content,
"number": [1],
"sub_items": [],
"path": "chapter_1.md",
"source_path": "chapter_1.md",
"parent_names": []
}
}
],
"__non_exhaustive": null
}))
.unwrap()
}
fn mock_context(admonish: &Value, renderer: &str) -> PreprocessorContext {
let value = json!({
"root": "/path/to/book",
"config": {
"book": {
"authors": ["AUTHOR"],
"language": "en",
"multilingual": false,
"src": "src",
"title": "TITLE"
},
"preprocessor": {
"admonish": admonish,
}
},
"renderer": renderer,
"mdbook_version": "0.4.21"
});
serde_json::from_value(value).unwrap()
}
#[test]
fn run_html() {
let content = r#"
````admonish title="Title"
```rust
let x = 10;
x = 20;
```
````
"#;
let expected_content = r##"
<div id="admonition-title" class="admonition admonish-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": "3.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": "3.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": "3.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)
}
}

125
src/render.rs Normal file
View File

@@ -0,0 +1,125 @@
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 => "admonish-note",
Directive::Abstract => "admonish-abstract",
Directive::Info => "admonish-info",
Directive::Tip => "admonish-tip",
Directive::Success => "admonish-success",
Directive::Question => "admonish-question",
Directive::Warning => "admonish-warning",
Directive::Failure => "admonish-failure",
Directive::Danger => "admonish-danger",
Directive::Bug => "admonish-bug",
Directive::Example => "admonish-example",
Directive::Quote => "admonish-quote",
}
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct Admonition<'a> {
pub(crate) directive: Directive,
pub(crate) title: String,
pub(crate) content: Cow<'a, str>,
pub(crate) additional_classnames: Vec<String>,
pub(crate) collapsible: bool,
pub(crate) indent: usize,
}
impl<'a> Admonition<'a> {
pub(crate) fn new(info: AdmonitionMeta, content: &'a str, indent: usize) -> Self {
let AdmonitionMeta {
directive,
title,
additional_classnames,
collapsible,
} = info;
Self {
directive,
title,
content: Cow::Borrowed(content),
additional_classnames,
collapsible,
indent,
}
}
pub(crate) fn html_with_unique_ids(&self, id_counter: &mut HashMap<String, usize>) -> String {
let anchor_id = unique_id_from_content(
if !self.title.is_empty() {
&self.title
} else {
ANCHOR_ID_DEFAULT
},
id_counter,
);
self.html(&anchor_id)
}
fn html(&self, anchor_id: &str) -> String {
let mut additional_class = Cow::Borrowed(self.directive.classname());
let title = &self.title;
let content = &self.content;
let indent = " ".repeat(self.indent);
let title_block = if self.collapsible { "summary" } else { "div" };
let title_html = if !title.is_empty() {
Cow::Owned(format!(
r##"{indent}<{title_block} class="admonition-title">
{indent}
{indent}{title}
{indent}
{indent}<a class="admonition-anchor-link" href="#{ANCHOR_ID_PREFIX}-{anchor_id}"></a>
{indent}</{title_block}>
"##
))
} else {
Cow::Borrowed("")
};
if !self.additional_classnames.is_empty() {
let mut buffer = additional_class.into_owned();
for additional_classname in &self.additional_classnames {
buffer.push(' ');
buffer.push_str(additional_classname);
}
additional_class = Cow::Owned(buffer);
}
let admonition_block = if self.collapsible { "details" } else { "div" };
// Notes on the HTML template:
// - the additional whitespace around the content are deliberate
// In line with the commonmark spec, this allows the inner content to be
// rendered as markdown paragraphs.
format!(
r#"
{indent}<{admonition_block} id="{ANCHOR_ID_PREFIX}-{anchor_id}" class="admonition {additional_class}">
{title_html}{indent}<div>
{indent}
{indent}{content}
{indent}
{indent}</div>
{indent}</{admonition_block}>"#,
)
}
/// Strips all admonish syntax, leaving the plain content of the block.
pub(crate) fn strip(&self) -> String {
// Add in newlines to preserve line numbering for test output
// These replace the code fences we stripped out
format!("\n{}\n", self.content)
}
}
const ANCHOR_ID_PREFIX: &str = "admonition";
const ANCHOR_ID_DEFAULT: &str = "default";

116
src/resolve.rs Normal file
View 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
View 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,
}