mirror of
https://github.com/tommilligan/mdbook-admonish.git
synced 2025-12-27 13:30:47 -05:00
Compare commits
175 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea6c0a687a | ||
|
|
50b9187ee5 | ||
|
|
fbd07ecaf0 | ||
|
|
37d03b872e | ||
|
|
fa91fe3778 | ||
|
|
d59dce3533 | ||
|
|
21d86546ff | ||
|
|
79ee592f56 | ||
|
|
0d3eb11a2c | ||
|
|
43579f902b | ||
|
|
7ee3643dc3 | ||
|
|
c4bf2608e7 | ||
|
|
d4667a5f55 | ||
|
|
8751b3cd24 | ||
|
|
516bbc6e7e | ||
|
|
fc5c5eae39 | ||
|
|
147a9bdb98 | ||
|
|
540ea8e5e8 | ||
|
|
ae4bac21ba | ||
|
|
f0c4d3764e | ||
|
|
a942a8094e | ||
|
|
d10a427b0f | ||
|
|
41a3b05094 | ||
|
|
d0f73baa3f | ||
|
|
4c40418090 | ||
|
|
30c9ad2269 | ||
|
|
f7482415a7 | ||
|
|
08397fbffa | ||
|
|
b78fc39031 | ||
|
|
fe7b475753 | ||
|
|
a56976d085 | ||
|
|
08967d550d | ||
|
|
8fa1411095 | ||
|
|
a1e5cfa48d | ||
|
|
33fd522d68 | ||
|
|
4805398359 | ||
|
|
c681ff922d | ||
|
|
cd0726aaf2 | ||
|
|
a08967e073 | ||
|
|
f7e6970fa3 | ||
|
|
5d2124b319 | ||
|
|
d79ebb4fad | ||
|
|
9df896cd77 | ||
|
|
ffb819c315 | ||
|
|
f278374c88 | ||
|
|
2c18292401 | ||
|
|
294af2478c | ||
|
|
278d17792b | ||
|
|
9f6c73091a | ||
|
|
9f221abc12 | ||
|
|
82c7bd4fd9 | ||
|
|
9bca2a66df | ||
|
|
80cce8480c | ||
|
|
5d5b73ded6 | ||
|
|
c17a66440c | ||
|
|
068a375647 | ||
|
|
f04016d017 | ||
|
|
3f8bf86ac3 | ||
|
|
8045e217c9 | ||
|
|
85fde44c09 | ||
|
|
a2c3e49ef9 | ||
|
|
8a0ecc5dd1 | ||
|
|
467f5f8f13 | ||
|
|
c4a05dae9d | ||
|
|
1bb14684c4 | ||
|
|
b3798b4d9f | ||
|
|
26c344a1e6 | ||
|
|
b1e6a5ee1e | ||
|
|
0d7b64d1b8 | ||
|
|
9730cd0aaa | ||
|
|
dc219f755d | ||
|
|
a2524f890b | ||
|
|
c3207e4d16 | ||
|
|
ab63c90231 | ||
|
|
d5bdde1f5c | ||
|
|
52ca8fc831 | ||
|
|
e1ea411e9a | ||
|
|
31d5a27a6d | ||
|
|
0f0e02702c | ||
|
|
7235d5f349 | ||
|
|
0304995dbb | ||
|
|
197d9cd059 | ||
|
|
04ff932f1f | ||
|
|
1526a5d814 | ||
|
|
eb21495797 | ||
|
|
ebe6f7815c | ||
|
|
c0c953c865 | ||
|
|
0fa34a66a0 | ||
|
|
dfc12c3652 | ||
|
|
108edfffc5 | ||
|
|
38f1933e8f | ||
|
|
a839038263 | ||
|
|
8501c812d9 | ||
|
|
933432afb2 | ||
|
|
496e8f7c6d | ||
|
|
1877fd1731 | ||
|
|
20286b3fae | ||
|
|
8e68cf919f | ||
|
|
02640dab1f | ||
|
|
771e9c9fd8 | ||
|
|
cce9343c47 | ||
|
|
20b158966b | ||
|
|
491f9cf341 | ||
|
|
6deaf1ea2b | ||
|
|
041e5a566f | ||
|
|
99b5a235cf | ||
|
|
39edc4d92a | ||
|
|
7773213093 | ||
|
|
e888fcd021 | ||
|
|
95dc7582ad | ||
|
|
b658eb6049 | ||
|
|
623291625a | ||
|
|
4dad5a86c8 | ||
|
|
7e774f4655 | ||
|
|
823cefbcbc | ||
|
|
a6a2941821 | ||
|
|
faf99a1b76 | ||
|
|
afdc2b03d0 | ||
|
|
e55df3e60b | ||
|
|
f3d49b93de | ||
|
|
92caf95b34 | ||
|
|
0742c6c1e8 | ||
|
|
60706be3e0 | ||
|
|
f5a6b9ef0f | ||
|
|
9361b7e7fa | ||
|
|
24bef47b15 | ||
|
|
0eb5fd35c3 | ||
|
|
de539cd0fd | ||
|
|
4842daea1c | ||
|
|
76212fccfb | ||
|
|
681c991a9a | ||
|
|
1e0a3992d5 | ||
|
|
d97747d195 | ||
|
|
2e2cebfc83 | ||
|
|
ab595f18f7 | ||
|
|
196585f4f2 | ||
|
|
7ad4d3f18c | ||
|
|
0324c93efa | ||
|
|
62dd36624d | ||
|
|
b3e82df34e | ||
|
|
e8813eb104 | ||
|
|
f606ad8758 | ||
|
|
d269838765 | ||
|
|
082359e562 | ||
|
|
84d163c32f | ||
|
|
90484a44ea | ||
|
|
5e7674d1f9 | ||
|
|
55a654dfca | ||
|
|
a2f664cac3 | ||
|
|
dfb70b0415 | ||
|
|
fb1b789386 | ||
|
|
97bcd97c64 | ||
|
|
a2a7316b26 | ||
|
|
f9eb198cd0 | ||
|
|
5530da074b | ||
|
|
0fe2ad52ed | ||
|
|
787744f1f1 | ||
|
|
603c83e2eb | ||
|
|
fbdffaa723 | ||
|
|
7c20d5b2d1 | ||
|
|
3507d6b5e0 | ||
|
|
f81d4d40dd | ||
|
|
ed019a92d9 | ||
|
|
9dd2ca128c | ||
|
|
51120acfd9 | ||
|
|
650123645b | ||
|
|
438c1dff5a | ||
|
|
78b7451e49 | ||
|
|
bb937dc2d2 | ||
|
|
db7101cb12 | ||
|
|
fd6c2d0bd0 | ||
|
|
72deb8421c | ||
|
|
7b5a13d6af | ||
|
|
28dfc5b6c3 | ||
|
|
beb640077f |
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
62
.github/workflows/check.yml
vendored
62
.github/workflows/check.yml
vendored
@@ -6,12 +6,12 @@ jobs:
|
||||
# Fast test before we kick off all the other jobs
|
||||
fast-test:
|
||||
name: Fast test
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
@@ -19,10 +19,9 @@ jobs:
|
||||
target
|
||||
key: fast-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Install more toolchain
|
||||
run: rustup component add rustfmt clippy
|
||||
- name: Run tests
|
||||
@@ -32,28 +31,36 @@ jobs:
|
||||
detailed-test:
|
||||
needs: fast-test
|
||||
name: Test main target
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
~/.cargo/bin
|
||||
cargo_target
|
||||
/tmp/cargo-install-target-dir
|
||||
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Install node toolchain
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: compile_assets/yarn.lock
|
||||
- name: Install additional test dependencies
|
||||
env:
|
||||
CARGO_TARGET_DIR: cargo_target
|
||||
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
|
||||
run: ./scripts/install
|
||||
- name: Run check script
|
||||
run: ./scripts/check
|
||||
@@ -65,31 +72,41 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-20.04
|
||||
# - windows-2019
|
||||
- ubuntu-24.04
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- 1.58.0
|
||||
- 1.82.0
|
||||
experimental:
|
||||
- false
|
||||
# Run a canary test on nightly that's allowed to fail
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
# Run a canary test on nightly that's allowed to fail
|
||||
- os: ubuntu-24.04
|
||||
rust: nightly
|
||||
experimental: true
|
||||
# Don't bother retesting stable linux, we did it in the comprehensive test
|
||||
# Test only stable on Windows, presume we'd get same result on other
|
||||
# versions as Linux
|
||||
- os: windows-2022
|
||||
rust: stable
|
||||
experimental: false
|
||||
exclude:
|
||||
- os: ubuntu-20.04
|
||||
# Don't bother retesting stable linux, we did it in the comprehensive test
|
||||
- os: ubuntu-24.04
|
||||
rust: stable
|
||||
experimental: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
steps:
|
||||
# This is required, otherwise we get files with CRLF on Windows
|
||||
# Which causes tests relying on data loaded from files to fail
|
||||
- name: Set git to use LF everywhere
|
||||
run: |
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
@@ -97,9 +114,8 @@ jobs:
|
||||
target
|
||||
key: test-${{ matrix.os }}-${{ matrix.rust }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
- name: Run tests
|
||||
run: cargo test
|
||||
|
||||
84
.github/workflows/deploy.yml
vendored
84
.github/workflows/deploy.yml
vendored
@@ -16,14 +16,16 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- x86_64-unknown-linux-gnu
|
||||
- x86_64-unknown-linux-musl
|
||||
- x86_64-apple-darwin
|
||||
- x86_64-pc-windows-msvc
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
name: aarch64-unknown-linux-musl.tar.gz
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
# Deliberately pinned to the same version `mdbook` uses to build
|
||||
# binaries, so we use the same glibc version
|
||||
#
|
||||
# ref: https://github.com/rust-lang/mdBook/pull/1955
|
||||
os: ubuntu-24.04
|
||||
name: x86_64-unknown-linux-gnu.tar.gz
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
@@ -37,40 +39,42 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Setup | Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Cache files between builds
|
||||
- name: Setup | Cache Cargo
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
# Note that we don't cache the `target` directory here
|
||||
# so we do a completely clean rebuild for artefacts
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Setup | Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
profile: minimal
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Setup | musl tools
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
run: sudo apt install -y musl-tools
|
||||
- name: Setup | cross
|
||||
if: endsWith(matrix.target, '-unknown-linux-musl')
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cross
|
||||
|
||||
- name: Build | Build
|
||||
if: matrix.target != 'x86_64-unknown-linux-musl'
|
||||
if: ${{ !endsWith(matrix.target, '-unknown-linux-musl') }}
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Build | Build (musl)
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
if: endsWith(matrix.target, '-unknown-linux-musl')
|
||||
run: cross build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Post Setup | Extract tag name
|
||||
shell: bash
|
||||
run: echo "##[set-output name=tag;]$(echo ${GITHUB_REF#refs/tags/})"
|
||||
run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
id: extract_tag
|
||||
|
||||
- name: Post Setup | Prepare artifacts [Windows]
|
||||
@@ -78,7 +82,6 @@ jobs:
|
||||
run: |
|
||||
mkdir target/stage
|
||||
cd target/${{ matrix.target }}/release
|
||||
strip ${{ env.CRATE_NAME }}.exe
|
||||
7z a ../../stage/${{ env.CRATE_NAME }}-${{ steps.extract_tag.outputs.tag }}-${{ matrix.name }} ${{ env.CRATE_NAME }}.exe
|
||||
cd -
|
||||
- name: Post Setup | Prepare artifacts [-nix]
|
||||
@@ -86,14 +89,15 @@ jobs:
|
||||
run: |
|
||||
mkdir target/stage
|
||||
cd target/${{ matrix.target }}/release
|
||||
strip ${{ env.CRATE_NAME }}
|
||||
tar czvf ../../stage/${{ env.CRATE_NAME }}-${{ steps.extract_tag.outputs.tag }}-${{ matrix.name }} ${{ env.CRATE_NAME }}
|
||||
cd -
|
||||
- name: Post Setup | Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.CRATE_NAME }}-${{ steps.extract_tag.outputs.tag }}-${{ matrix.name }}
|
||||
path: target/stage/*
|
||||
# Idempotency: overwrite artefact by name if we're rerunning the deployment
|
||||
overwrite: true
|
||||
|
||||
# Create GitHub release with Rust build targets and release notes
|
||||
github_release:
|
||||
@@ -102,20 +106,50 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup | Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup | Artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Setup | Extract version
|
||||
shell: bash
|
||||
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
id: extract_version
|
||||
|
||||
- name: Setup | Release notes
|
||||
run: |
|
||||
git log -1 --pretty='%s' > RELEASE.md
|
||||
cat CHANGELOG.md | sed -n '/^## ${{ steps.extract_version.outputs.version }}$/,/^## /p' | sed '$d' > RELEASE.md
|
||||
- name: Build | Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
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-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
~/.cargo/bin
|
||||
# We reuse the cache from our detailed test environment, if available
|
||||
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Publish crate
|
||||
env:
|
||||
CARGO_LOGIN_TOKEN: ${{ secrets.CARGO_LOGIN_TOKEN }}
|
||||
run: ./scripts/publish
|
||||
|
||||
45
.github/workflows/docs.yml
vendored
Normal file
45
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
~/.cargo/bin
|
||||
/tmp/cargo-install-target-dir
|
||||
# We reuse the cache from our detailed test environment, if available
|
||||
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Install mdbook
|
||||
env:
|
||||
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
|
||||
run: ./scripts/install-mdbook
|
||||
- name: Install mdbook extras
|
||||
env:
|
||||
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
|
||||
run: ./book/scripts/install-mdbook-extras
|
||||
- name: Build book
|
||||
run: ./scripts/build-book
|
||||
- name: Push docs
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: book/book
|
||||
31
.github/workflows/publish.yml
vendored
31
.github/workflows/publish.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
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
|
||||
213
CHANGELOG.md
213
CHANGELOG.md
@@ -1,4 +1,209 @@
|
||||
## Changelog
|
||||
# Changelog
|
||||
|
||||
## v1.20.0
|
||||
|
||||
### Changed
|
||||
|
||||
- MSRV (minimum supported rust version) is now 1.82.0 ([#227](https://github.com/tommilligan/mdbook-admonish/pull/227))
|
||||
- Collapsible blocks now show content in print view ([#228](https://github.com/tommilligan/mdbook-admonish/pull/228)). Thanks to [@igor-petruk](https://github.com/igor-petruk) for raising this issue.
|
||||
|
||||
## v1.19.0
|
||||
|
||||
### Changed
|
||||
|
||||
- MSRV (minimum supported rust version) is now 1.76.0 ([#208](https://github.com/tommilligan/mdbook-admonish/pull/208))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed blocks not rendering correctly in indented list items. Thanks to [@JorelAli](https://github.com/JorelAli) for the bug report! ([#224](https://github.com/tommilligan/mdbook-admonish/pull/224))
|
||||
|
||||
## v1.18.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Add ARIA attributes to generated blocks. Thanks to [@toastal](https://github.com/toastal) for suggesting this feature! ([#195](https://github.com/tommilligan/mdbook-admonish/pull/195))
|
||||
- Note: This subtly alters the emitted HTML, and could cause additional styles applied to blocks to break. Native `mdbook-admonish` styles are not affected.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed some valid configurations producing TOML serialization errors. Thanks to [@DianaNites](https://github.com/DianaNites) for reporting this! ([#197](https://github.com/tommilligan/mdbook-admonish/pull/197))
|
||||
|
||||
## v1.17.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed a stray debug statement ([#186](https://github.com/tommilligan/mdbook-admonish/pull/186))
|
||||
|
||||
## v1.17.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Blocks should have key-value options separated by commas. Existing syntax remains is supported for back-compatibility. See [the documentation on Additional Options](https://tommilligan.github.io/mdbook-admonish/#additional-options) for more details ([#181](https://github.com/tommilligan/mdbook-admonish/pull/181))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Titles contining `=` will now render correctly. Thanks to [@s00500](https://github.com/s00500) for the bug report! ([#181](https://github.com/tommilligan/mdbook-admonish/pull/181))
|
||||
|
||||
## v1.16.0
|
||||
|
||||
### Changed
|
||||
|
||||
- MSRV (minimum supported rust version) is now 1.74.0 ([#175](https://github.com/tommilligan/mdbook-admonish/pull/175))
|
||||
- `custom` directives should now be configured under the `directive.custom` option. Existing `custom` configurations are supported for back compatibility ([#179](https://github.com/tommilligan/mdbook-admonish/pull/174))
|
||||
|
||||
### Added
|
||||
|
||||
- Make blocks `collapsible` on a per-directive basis. Thanks to [@yannickseurin](https://github.com/yannickseurin) for contributing this feature! ([#174](https://github.com/tommilligan/mdbook-admonish/pull/174))
|
||||
|
||||
### Fixed
|
||||
|
||||
- The `css_id_prefix` option now uses snake case for consistency (kebab case remains supported for back compatibility). Thanks to [@yannickseurin](https://github.com/yannickseurin) for fixing this! ([#173](https://github.com/tommilligan/mdbook-admonish/pull/173))
|
||||
|
||||
## 1.15.0
|
||||
|
||||
### Added
|
||||
|
||||
- Support [custom directives](https://tommilligan.github.io/mdbook-admonish/overview.html#custom-blocks) with the new `mdbook-admonish generate-custom` helper. See the [mdbook-admonish book](https://tommilligan.github.io/mdbook-admonish/overview.html#custom-blocks) for guidance. Thanks to [@Sky9x](https://github.com/Sky9x) for helping design this feature! ([#165](https://github.com/tommilligan/mdbook-admonish/pull/165))
|
||||
|
||||
### Fixed
|
||||
|
||||
- `additional-css` unix style path normalization. Thanks to [@carlocorradini](https://github.com/carlocorradini) for reporting and fixing! ([#163](https://github.com/tommilligan/mdbook-admonish/pull/163))
|
||||
|
||||
## 1.14.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Styles version updated to `3.0.1`. Run `mdbook-admonish install` to update.
|
||||
|
||||
### Added
|
||||
|
||||
- You can now set custom CSS ids for admonition blocks with the `id` field. Thanks to [@Sky9x](https://github.com/Sky9x) for contributing this feature! ([#144](https://github.com/tommilligan/mdbook-admonish/pull/144))
|
||||
- You can also now customize the CSS id prefix with the config option `default.css_id_prefix`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve rendering of blocks in print/PDF view. Thanks to [@csk111165](https://github.com/csk111165) for the report ([#152](https://github.com/tommilligan/mdbook-admonish/issues/152))
|
||||
- Fix the default titles for `tldr` and `faq` directives looking bad. They now render as `TL;DR` and `FAQ` by default. Thanks [@joshka](https://github.com/joshka) for fixing this! ([#154](https://github.com/tommilligan/mdbook-admonish/pull/154))
|
||||
|
||||
## 1.13.1
|
||||
|
||||
### Changed
|
||||
|
||||
- Bumped internal `mdbook` version to `0.4.35` ([#142](https://github.com/tommilligan/mdbook-admonish/pull/142))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Relaxed `clap` dependency to fix compilation error when using other `mdbook-*` plugins. Thanks to [@joshka](https://github.com/joshka) for the [report](https://github.com/tommilligan/mdbook-admonish/pull/141)! ([#142](https://github.com/tommilligan/mdbook-admonish/pull/142))
|
||||
|
||||
## 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 version 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
|
||||
|
||||
@@ -67,7 +272,11 @@ This behaviour is [documented in the readme here](https://github.com/tommilligan
|
||||
- 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
|
||||
## 1.3.0 (yanked)
|
||||
|
||||
**Note:** This release has been yanked.
|
||||
|
||||
It unintentionally introduced a serious parsing bug.
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
31
CONTRIBUTING.md
Normal file
31
CONTRIBUTING.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Contributing
|
||||
|
||||
## Workflow
|
||||
|
||||
Please submit a PR from a **new branch** in your fork.
|
||||
Please do not submit a PR from your fork's `main` branch, as it makes collaborating on/editing the branch a pain.
|
||||
|
||||
## Project design
|
||||
|
||||
- Compiled CSS styles are built and committed from SCSS sources. See the `compile_assets` folder for details.
|
||||
- `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.
|
||||
2332
Cargo.lock
generated
2332
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
39
Cargo.toml
39
Cargo.toml
@@ -1,12 +1,13 @@
|
||||
[package]
|
||||
name = "mdbook-admonish"
|
||||
version = "1.6.0"
|
||||
version = "1.20.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.76.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,28 +18,40 @@ name = "mdbook-admonish"
|
||||
path = "src/bin/mdbook-admonish.rs"
|
||||
required-features = ["cli"]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
[lib]
|
||||
name = "mdbook_admonish"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.57"
|
||||
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"
|
||||
semver = "1.0.7"
|
||||
serde_json = "1.0.79"
|
||||
toml_edit = { version = "0.13.4", optional = true }
|
||||
anyhow = "1.0.86"
|
||||
# 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.5", default-features = false, features = ["std", "derive"], optional = true }
|
||||
env_logger = { version = "0.11", default-features = false, optional = true }
|
||||
log = "0.4.21"
|
||||
mdbook = "0.4.40"
|
||||
once_cell = "1.19.0"
|
||||
path-slash = "0.2.1"
|
||||
pulldown-cmark = "0.13"
|
||||
regex = "1.10.5"
|
||||
semver = "1.0.23"
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
toml = "0.8.14"
|
||||
toml_edit = { version = "0.22.14", optional = true }
|
||||
hex_color = { version = "3.0.0", features = ["serde"] }
|
||||
|
||||
[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"]
|
||||
cli = ["clap", "env_logger"]
|
||||
# Enable installation of files and configuration
|
||||
cli-install = ["toml_edit"]
|
||||
|
||||
139
README.md
139
README.md
@@ -1,7 +1,7 @@
|
||||
# mdbook-admonish
|
||||
|
||||
[](https://crates.io/crates/mdbook-admonish)
|
||||
[](https://docs.rs/mdbook-admonish)
|
||||
[](https://tommilligan.github.io/mdbook-admonish/)
|
||||
|
||||
A preprocessor for [mdbook](https://github.com/rust-lang-nursery/mdBook) to add [Material Design](https://material.io/design) admonishments, based on the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) implementation.
|
||||
|
||||
@@ -19,11 +19,13 @@ into this:
|
||||
|
||||
## Examples
|
||||
|
||||
Read the usage and reference [here](https://tommilligan.github.io/mdbook-admonish/), to see the actual examples in action. You can see the source in the [`./book`](./book) subdirectory.
|
||||
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:
|
||||
Projects using mdbook-admonish include:
|
||||
|
||||
- [The Rhai Book](https://rhai.rs/book/)
|
||||
- [The Rhai Book](https://rhai.rs/book/) ([source](https://github.com/rhaiscript/book))
|
||||
- [The Trunk Guide](https://trunkrs.dev/guide/) ([source](https://github.com/trunk-rs/trunk))
|
||||
- [PRQL language book](https://prql-lang.org/book/) ([source](https://github.com/PRQL/prql))
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -58,72 +60,11 @@ A plain note.
|
||||
|
||||
### Additional Options
|
||||
|
||||
#### Custom title
|
||||
See the [`mdbook-admonish` book](https://tommilligan.github.io/mdbook-admonish/) for additional options, such as:
|
||||
|
||||
A custom title can be provided, contained in a double quoted JSON string.
|
||||
Note that JSON escapes must be escaped again - for instance, write `\"` as `\\"`.
|
||||
|
||||
````
|
||||
```admonish warning "Data loss"
|
||||
The following steps can lead to irrecoverable data corruption.
|
||||
```
|
||||
````
|
||||
|
||||

|
||||
|
||||
You can also remove the title bar entirely, by specifying the empty string:
|
||||
|
||||
````
|
||||
```admonish success ""
|
||||
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 "_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!"
|
||||
```
|
||||
~~~
|
||||
````
|
||||
|
||||

|
||||
|
||||
#### Custom styling
|
||||
|
||||
If you want to provide custom styling to a specific admonition, you can attach one or more custom classnames:
|
||||
|
||||
````
|
||||
```admonish note.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>
|
||||
```
|
||||
- Custom titles
|
||||
- Custom styling
|
||||
- Collapsible blocks
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -131,6 +72,9 @@ 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:
|
||||
@@ -162,6 +106,21 @@ 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.
|
||||
@@ -178,12 +137,17 @@ ERROR:
|
||||
|
||||
If you want to update across minor versions without breakage, you should always run `mdbook-admonish install`.
|
||||
|
||||
Alternatively, pin to a specific version for a reproducible installation:
|
||||
### Process included files
|
||||
|
||||
```bash
|
||||
cargo install mdbook-admonish --vers "1.5.0" --locked
|
||||
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:
|
||||
@@ -191,36 +155,15 @@ 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
|
||||
|
||||
- Compiled CSS styles are built and committed from SCSS sources. See the `compile_assets` folder for details.
|
||||
- `mdbook-admonish install` is responsible for delivering additional assets and configuration to a client book.
|
||||
- `mdbook-admonish` is responsible for preprocessing book data, adding HTML that references compiled classnames.
|
||||
|
||||
### Scripts to get started
|
||||
|
||||
- `./scripts/install` installs other toolchains required for development
|
||||
- `./scripts/check` runs a full CI check
|
||||
- `./scripts/rebuild-book` rebuilds the reference book under `./book`. This is useful for integration testing locally.
|
||||
|
||||
### Making breaking changes in CSS
|
||||
|
||||
To make a breaking change in CSS, you should:
|
||||
|
||||
- Update the assets version in `./src/bin/assets/VERSION`
|
||||
- Update the required assets version specifier in `./src/REQUIRED_ASSETS_VERSION`
|
||||
|
||||
You must make the next `mdbook-admonish` crate version at least a **minor** version bump.
|
||||
|
||||
### Releasing
|
||||
|
||||
Github workflows are setup such that pushing a `vX.Y.Z` tag will trigger a release to be cut.
|
||||
|
||||
Once the release is created, copy and paste the relevant section of `CHANGELOG.md` manually to update the description.
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on developing.
|
||||
|
||||
## Thanks
|
||||
|
||||
|
||||
@@ -4,14 +4,22 @@ language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "The mdbook-admonish book"
|
||||
git-repository-url = "https://github.com/tommilligan/mdbook-admonish"
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "1.0.0" # do not edit: managed by `mdbook-admonish install`
|
||||
assets_version = "3.1.0" # do not edit: managed by `mdbook-admonish install`
|
||||
|
||||
[preprocessor.admonish.directive.custom]
|
||||
expensive = { icon = "./money-bag.svg", color = "#24ab38" }
|
||||
|
||||
[preprocessor.toc]
|
||||
command = "mdbook-toc"
|
||||
renderer = ["html"]
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["././mdbook-admonish.css"]
|
||||
additional-css = ["./mdbook-admonish.css", "./mdbook-admonish-custom.css"]
|
||||
|
||||
20
book/mdbook-admonish-custom.css
Normal file
20
book/mdbook-admonish-custom.css
Normal file
@@ -0,0 +1,20 @@
|
||||
:root {
|
||||
--md-admonition-icon--admonish-expensive: url("data:image/svg+xml;charset=utf-8,<svg width='800px' height='800px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><!-- ref: https://www.svgrepo.com/svg/526038/money-bag --><!-- Used under CC Attribution License; Author: Solar Icons --><path fill-rule='evenodd' clip-rule='evenodd' d='M12.052 1.25H11.948C11.0495 1.24997 10.3003 1.24995 9.70552 1.32991C9.07773 1.41432 8.51093 1.59999 8.05546 2.05546C7.59999 2.51093 7.41432 3.07773 7.32991 3.70552C7.27259 4.13189 7.25637 5.15147 7.25179 6.02566C5.22954 6.09171 4.01536 6.32778 3.17157 7.17157C2 8.34315 2 10.2288 2 14C2 17.7712 2 19.6569 3.17157 20.8284C4.34314 22 6.22876 22 9.99998 22H14C17.7712 22 19.6569 22 20.8284 20.8284C22 19.6569 22 17.7712 22 14C22 10.2288 22 8.34315 20.8284 7.17157C19.9846 6.32778 18.7705 6.09171 16.7482 6.02566C16.7436 5.15147 16.7274 4.13189 16.6701 3.70552C16.5857 3.07773 16.4 2.51093 15.9445 2.05546C15.4891 1.59999 14.9223 1.41432 14.2945 1.32991C13.6997 1.24995 12.9505 1.24997 12.052 1.25ZM15.2479 6.00188C15.2434 5.15523 15.229 4.24407 15.1835 3.9054C15.1214 3.44393 15.0142 3.24644 14.8839 3.11612C14.7536 2.9858 14.5561 2.87858 14.0946 2.81654C13.6116 2.7516 12.964 2.75 12 2.75C11.036 2.75 10.3884 2.7516 9.90539 2.81654C9.44393 2.87858 9.24644 2.9858 9.11612 3.11612C8.9858 3.24644 8.87858 3.44393 8.81654 3.9054C8.771 4.24407 8.75661 5.15523 8.75208 6.00188C9.1435 6 9.55885 6 10 6H14C14.4412 6 14.8565 6 15.2479 6.00188ZM12 9.25C12.4142 9.25 12.75 9.58579 12.75 10V10.0102C13.8388 10.2845 14.75 11.143 14.75 12.3333C14.75 12.7475 14.4142 13.0833 14 13.0833C13.5858 13.0833 13.25 12.7475 13.25 12.3333C13.25 11.9493 12.8242 11.4167 12 11.4167C11.1758 11.4167 10.75 11.9493 10.75 12.3333C10.75 12.7174 11.1758 13.25 12 13.25C13.3849 13.25 14.75 14.2098 14.75 15.6667C14.75 16.857 13.8388 17.7155 12.75 17.9898V18C12.75 18.4142 12.4142 18.75 12 18.75C11.5858 18.75 11.25 18.4142 11.25 18V17.9898C10.1612 17.7155 9.25 16.857 9.25 15.6667C9.25 15.2525 9.58579 14.9167 10 14.9167C10.4142 14.9167 10.75 15.2525 10.75 15.6667C10.75 16.0507 11.1758 16.5833 12 16.5833C12.8242 16.5833 13.25 16.0507 13.25 15.6667C13.25 15.2826 12.8242 14.75 12 14.75C10.6151 14.75 9.25 13.7903 9.25 12.3333C9.25 11.143 10.1612 10.2845 11.25 10.0102V10C11.25 9.58579 11.5858 9.25 12 9.25Z' fill='%231C274C'/></svg>");
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-expensive) {
|
||||
border-color: #24ab38;
|
||||
}
|
||||
|
||||
:is(.admonish-expensive) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(36, 171, 56, 0.1);
|
||||
}
|
||||
:is(.admonish-expensive) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #24ab38;
|
||||
mask-image: var(--md-admonition-icon--admonish-expensive);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-expensive);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
6
book/money-bag.svg
Normal file
6
book/money-bag.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!-- ref: https://www.svgrepo.com/svg/526038/money-bag -->
|
||||
<!-- Used under CC Attribution License; Author: Solar Icons -->
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.052 1.25H11.948C11.0495 1.24997 10.3003 1.24995 9.70552 1.32991C9.07773 1.41432 8.51093 1.59999 8.05546 2.05546C7.59999 2.51093 7.41432 3.07773 7.32991 3.70552C7.27259 4.13189 7.25637 5.15147 7.25179 6.02566C5.22954 6.09171 4.01536 6.32778 3.17157 7.17157C2 8.34315 2 10.2288 2 14C2 17.7712 2 19.6569 3.17157 20.8284C4.34314 22 6.22876 22 9.99998 22H14C17.7712 22 19.6569 22 20.8284 20.8284C22 19.6569 22 17.7712 22 14C22 10.2288 22 8.34315 20.8284 7.17157C19.9846 6.32778 18.7705 6.09171 16.7482 6.02566C16.7436 5.15147 16.7274 4.13189 16.6701 3.70552C16.5857 3.07773 16.4 2.51093 15.9445 2.05546C15.4891 1.59999 14.9223 1.41432 14.2945 1.32991C13.6997 1.24995 12.9505 1.24997 12.052 1.25ZM15.2479 6.00188C15.2434 5.15523 15.229 4.24407 15.1835 3.9054C15.1214 3.44393 15.0142 3.24644 14.8839 3.11612C14.7536 2.9858 14.5561 2.87858 14.0946 2.81654C13.6116 2.7516 12.964 2.75 12 2.75C11.036 2.75 10.3884 2.7516 9.90539 2.81654C9.44393 2.87858 9.24644 2.9858 9.11612 3.11612C8.9858 3.24644 8.87858 3.44393 8.81654 3.9054C8.771 4.24407 8.75661 5.15523 8.75208 6.00188C9.1435 6 9.55885 6 10 6H14C14.4412 6 14.8565 6 15.2479 6.00188ZM12 9.25C12.4142 9.25 12.75 9.58579 12.75 10V10.0102C13.8388 10.2845 14.75 11.143 14.75 12.3333C14.75 12.7475 14.4142 13.0833 14 13.0833C13.5858 13.0833 13.25 12.7475 13.25 12.3333C13.25 11.9493 12.8242 11.4167 12 11.4167C11.1758 11.4167 10.75 11.9493 10.75 12.3333C10.75 12.7174 11.1758 13.25 12 13.25C13.3849 13.25 14.75 14.2098 14.75 15.6667C14.75 16.857 13.8388 17.7155 12.75 17.9898V18C12.75 18.4142 12.4142 18.75 12 18.75C11.5858 18.75 11.25 18.4142 11.25 18V17.9898C10.1612 17.7155 9.25 16.857 9.25 15.6667C9.25 15.2525 9.58579 14.9167 10 14.9167C10.4142 14.9167 10.75 15.2525 10.75 15.6667C10.75 16.0507 11.1758 16.5833 12 16.5833C12.8242 16.5833 13.25 16.0507 13.25 15.6667C13.25 15.2826 12.8242 14.75 12 14.75C10.6151 14.75 9.25 13.7903 9.25 12.3333C9.25 11.143 10.1612 10.2845 11.25 10.0102V10C11.25 9.58579 11.5858 9.25 12 9.25Z" fill="#1C274C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
9
book/scripts/install-mdbook-extras
Executable file
9
book/scripts/install-mdbook-extras
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -exuo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/../..
|
||||
|
||||
if ! mdbook-toc --version; then
|
||||
cargo install mdbook-toc --version 0.14.1 --force
|
||||
fi
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
- [Overview](./overview.md)
|
||||
- [Reference](./reference.md)
|
||||
- [Examples](./examples.md)
|
||||
|
||||
15
book/src/examples.md
Normal file
15
book/src/examples.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Examples
|
||||
|
||||
## Combining multiple custom properties
|
||||
|
||||
Note that the comma `,` is used to seperate custom options.
|
||||
|
||||
````
|
||||
```admonish quote collapsible=true, title='A title that really <span style="color: #e70073">pops</span>'
|
||||
To really <b><span style="color: #e70073">grab</span></b> your reader's attention.
|
||||
```
|
||||
````
|
||||
|
||||
```admonish quote collapsible=true, title='A title that really <span style="color: #e70073">pops</span>'
|
||||
To really <b><span style="color: #e70073">grab</span></b> your reader's attention.
|
||||
```
|
||||
@@ -1,9 +1,13 @@
|
||||
# mdbook-admonish
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Intoduction
|
||||
|
||||
[](https://crates.io/crates/mdbook-admonish)
|
||||
[](https://docs.rs/mdbook-admonish)
|
||||
|
||||
A preprocessor for [mdbook](https://github.com/rust-lang-nursery/mdBook) to add [Material Design](https://material.io/design) admonishments, based on the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) implementation.
|
||||
A preprocessor for [mdbook](https://github.com/rust-lang/mdBook) to add [Material Design](https://material.io/design) admonishments, based on the [mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) implementation.
|
||||
|
||||
It turns this:
|
||||
|
||||
@@ -21,6 +25,8 @@ A beautifully styled message.
|
||||
|
||||
## Usage
|
||||
|
||||
### A basic `admonish` block
|
||||
|
||||
Use any [fenced code-block](https://spec.commonmark.org/0.30/#fenced-code-blocks) as you normally would, but annotate it with `admonish <admonition type>`:
|
||||
|
||||
````
|
||||
@@ -33,7 +39,7 @@ My example is the best!
|
||||
My example is the best!
|
||||
```
|
||||
|
||||
See the [mkdocs-material docs](https://squidfunk.github.io/mkdocs-material/reference/admonitions/#supported-types) for a list of supported admonitions. You'll find:
|
||||
See the [list of directives](./reference.md#directives) for a full list of supported admonitions. You'll find:
|
||||
|
||||
- `info`
|
||||
- `warning`
|
||||
@@ -54,32 +60,57 @@ A plain note.
|
||||
A plain note.
|
||||
```
|
||||
|
||||
### Invalid blocks
|
||||
|
||||
By default, if an `admonish` block cannot be parsed, an error will be rendered in the output:
|
||||
|
||||
````
|
||||
```admonish title="\j"
|
||||
This block will error
|
||||
```
|
||||
````
|
||||
|
||||
```admonish title="\j"
|
||||
This block will error
|
||||
```
|
||||
|
||||
You can also configure the build to fail loudly, by setting `on_failure = "bail"` in `book.toml`. See the [configuration reference](./reference.md#booktoml-configuration) for more details.
|
||||
|
||||
### Additional Options
|
||||
|
||||
You can pass additional options to each block. Options are given like a [TOML Inline Table](https://toml.io/en/v1.0.0#inline-table), as key-value pairs separated by commas.
|
||||
|
||||
`mdbook-admonish` parses options by wrapping your options in an inline table before parsing them, so please consult [The TOML Reference](https://toml.io) if you run into any syntax errors. Be aware that:
|
||||
|
||||
- Key-value pairs must be separated with a comma `,`
|
||||
- TOML escapes must be escaped again - for instance, write `\"` as `\\"`.
|
||||
- For complex strings such as HTML, you may want to use a [literal string](https://toml.io/en/v1.0.0#string) to avoid complex escape sequences
|
||||
|
||||
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 JSON string.
|
||||
Note that JSON escapes must be escaped again - for instance, write `\"` as `\\"`.
|
||||
A custom title can be provided:
|
||||
|
||||
````
|
||||
```admonish warning "Data loss"
|
||||
```admonish warning title="Data loss"
|
||||
The following steps can lead to irrecoverable data corruption.
|
||||
```
|
||||
````
|
||||
|
||||
```admonish warning "Data loss"
|
||||
```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 ""
|
||||
```admonish success title=""
|
||||
This will take a while, go and grab a drink of water.
|
||||
```
|
||||
````
|
||||
|
||||
```admonish success ""
|
||||
```admonish success title=""
|
||||
This will take a while, go and grab a drink of water.
|
||||
```
|
||||
|
||||
@@ -88,13 +119,13 @@ This will take a while, go and grab a drink of water.
|
||||
Markdown and HTML can be used in the inner content, as you'd expect:
|
||||
|
||||
````
|
||||
```admonish tip "_Referencing_ and <i>dereferencing</i>"
|
||||
```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 "_Referencing_ and <i>dereferencing</i>"
|
||||
```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>, `*`.
|
||||
```
|
||||
@@ -122,7 +153,7 @@ print "Hello, world!"
|
||||
If you want to provide custom styling to a specific admonition, you can attach one or more custom classnames:
|
||||
|
||||
````
|
||||
```admonish note.custom-0.custom-1
|
||||
```admonish note title="Stylish", class="custom-0 custom-1"
|
||||
Styled with my custom CSS class.
|
||||
```
|
||||
````
|
||||
@@ -134,3 +165,80 @@ Will yield something like the following HTML, which you can then apply styles to
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Custom CSS ID
|
||||
|
||||
If you want to customize the CSS `id` field, set `id="custom-id"`.
|
||||
This will ignore [`default.css_id_prefix`](reference.md#default).
|
||||
|
||||
The default id is a normalized version of the admonishment's title,
|
||||
prefixed with the `default.css_id_prefix`,
|
||||
with an appended number if multiple blocks would have the same id.
|
||||
|
||||
Setting the `id` field will _ignore_ all other ids and the duplicate counter.
|
||||
|
||||
````
|
||||
```admonish info title="My Info", id="my-special-info"
|
||||
Link to this block with `#my-special-info` instead of the default `#admonition-my-info`.
|
||||
```
|
||||
````
|
||||
|
||||
#### Collapsible
|
||||
|
||||
For a block to be initially collapsible, and then be openable, set `collapsible=true`:
|
||||
|
||||
````
|
||||
```admonish title="Sneaky", collapsible=true
|
||||
Content will be hidden initially.
|
||||
```
|
||||
````
|
||||
|
||||
Will yield something like the following HTML, which you can then apply styles to:
|
||||
|
||||
```admonish title="Sneaky", collapsible=true
|
||||
Content will be hidden initially.
|
||||
```
|
||||
|
||||
### Custom blocks
|
||||
|
||||
You can add new block types via the `book.toml` config:
|
||||
|
||||
```toml
|
||||
# book.toml
|
||||
|
||||
[[preprocessor.admonish.custom]]
|
||||
directive = "expensive"
|
||||
icon = "./money-bag.svg"
|
||||
color = "#24ab38"
|
||||
aliases = ["money", "cash", "budget"]
|
||||
```
|
||||
|
||||
You must then generate the relevant CSS file, and reference it in the `output.html` section.
|
||||
`mdbook-admonish` has a helper to quickly do this for you:
|
||||
|
||||
```bash
|
||||
# Generates a file at ./mdbook-admonish-custom.css with your styles in
|
||||
$ mdbook-admonish generate-custom ./mdbook-admonish-custom.css
|
||||
```
|
||||
|
||||
```toml
|
||||
# book.toml
|
||||
|
||||
[output.html]
|
||||
# Reference the new file, so it's bundled in with book styles
|
||||
additional-css = ["./mdbook-admonish.css", "./mdbook-admonish-custom.css"]
|
||||
```
|
||||
|
||||
You can then reference the new directive (or alias) like usual in your blocks.
|
||||
|
||||
````
|
||||
```admonish expensive
|
||||
Remember, this operation costs money!
|
||||
```
|
||||
````
|
||||
|
||||
```admonish expensive
|
||||
Remember, this operation costs money!
|
||||
```
|
||||
|
||||
You can also set a default `title`. See the [Reference](./reference.md) page for more details.
|
||||
|
||||
@@ -1,9 +1,147 @@
|
||||
# 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`.
|
||||
- `default.css_id_prefix` (optional, default: `"admonition-"`): The default css id prefix to add to the id of all blocks. Ignored on blocks with an `id` field.
|
||||
|
||||
### `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`.
|
||||
|
||||
### `directive`
|
||||
|
||||
Optional.
|
||||
|
||||
Settings relating to each type of block.
|
||||
|
||||
#### `builtin`
|
||||
|
||||
Optional.
|
||||
|
||||
Override the settings of a builtin directive.
|
||||
|
||||
The subkey of `builtin` is the directive to override. This must be the first directive listed in the [Directives](#directives) section below, e.g. `warning` (not `caution` or other aliases).
|
||||
|
||||
```toml
|
||||
[preprocessor.admonish.directive.builtin.warning]
|
||||
collapsible = true
|
||||
```
|
||||
|
||||
Subfields:
|
||||
|
||||
- `collapsible` (optional): The default boolean value of the collapsible property for this type of block.
|
||||
|
||||
#### `custom`
|
||||
|
||||
Optional.
|
||||
|
||||
Additional types of block to support. The subkey of `custom` is the new directive to support.
|
||||
|
||||
You must run `mdbook-admonish generate-custom` after updating these values, to generate the correct styles.
|
||||
|
||||
```toml
|
||||
[preprocessor.admonish.directive.custom.expensive]
|
||||
icon = "./money-bag.svg"
|
||||
color = "#24ab38"
|
||||
collapsible = true
|
||||
aliases = ["money", "cash", "budget"]
|
||||
```
|
||||
|
||||
Subfields:
|
||||
|
||||
- `icon`: A filepath relative to the book root to load an SVG icon from.
|
||||
- `color`: An RGB hex encoded color to use for the icon.
|
||||
- `collapsible` (optional): The default boolean value of the collapsible property for this type of block.
|
||||
- `aliases` (optional): One or more alternative directives to use this block.
|
||||
- `title` (optional): The default title for this type of block. If not specified, defaults to the directive in title case. To give each alias a custom title, add multiple custom blocks.
|
||||
|
||||
### `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.
|
||||
|
||||
Custom directives can be added via the `custom` config option above.
|
||||
|
||||
`note`
|
||||
|
||||
```admonish note
|
||||
|
||||
6
book/v2.md
Normal file
6
book/v2.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Notes for a v2 release
|
||||
|
||||
## Default behaviour changes
|
||||
|
||||
- `on_failure` default changed from `continue` to `bail`
|
||||
- `preprocessor.admonish.renderer.test.render_mode` default changed from `preserve` to `strip`
|
||||
2
compile_assets/.gitignore
vendored
2
compile_assets/.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
/node_modules
|
||||
*.css
|
||||
*.css.map
|
||||
.yarn/
|
||||
.pnp*
|
||||
|
||||
@@ -4,9 +4,17 @@
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "sass --no-source-map scss/mdbook-admonish.scss ../src/bin/assets/mdbook-admonish.css"
|
||||
"build": "yarn run build-prod && yarn run build-custom-expected",
|
||||
"build-prod": "sass --no-source-map scss/mdbook-admonish.scss ../src/bin/assets/mdbook-admonish.css",
|
||||
"build-custom-expected": "sass --no-source-map scss/mdbook-admonish-custom-expected.scss ../src/test_data/mdbook-admonish-custom-expected.css",
|
||||
"lint": "prettier --check .",
|
||||
"fix": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"sass": "^1.49.7"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.3"
|
||||
},
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6"
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
@use "sass:color";
|
||||
@use "sass:list";
|
||||
@use "./lib";
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Variables
|
||||
@@ -30,45 +31,48 @@
|
||||
/// 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;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Rules: layout
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// 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)}");
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Static content - base for all admonitions
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Admonition
|
||||
:is(.admonition) {
|
||||
@@ -84,10 +88,13 @@ $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
|
||||
// During print mode in browser, pdf renderer
|
||||
@media print {
|
||||
// Omit shadow as it may lead to rendering errors
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -116,26 +123,45 @@ $admonitions: (
|
||||
|
||||
// Anchor links
|
||||
a.admonition-anchor-link {
|
||||
&:link, &:visited {
|
||||
// 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 {
|
||||
&: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: 4.4rem 1.2rem;
|
||||
font-weight: 700;
|
||||
background-color: color.adjust($clr-blue-a200, $alpha: -0.9);
|
||||
// Always print title bar tint
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
// Compatilility with rendering markdown inside the content
|
||||
display: flex;
|
||||
|
||||
@@ -157,6 +183,9 @@ a.admonition-anchor-link {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: $clr-blue-a200;
|
||||
// Always print icon
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
|
||||
-webkit-mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
|
||||
mask-repeat: no-repeat;
|
||||
@@ -165,49 +194,65 @@ a.admonition-anchor-link {
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Rules: flavours
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@each $names, $props in $admonitions {
|
||||
$name: list.nth($names, 1);
|
||||
$tint: list.nth($props, 1);
|
||||
|
||||
// Admonition flavour selectors
|
||||
$flavours: ();
|
||||
@each $name in $names {
|
||||
$flavours: list.join($flavours, ".#{$name}", $separator: comma);
|
||||
}
|
||||
|
||||
// Admonition flavour
|
||||
:is(.admonition):is(#{$flavours}) {
|
||||
border-color: $tint;
|
||||
}
|
||||
|
||||
// Admonition flavour title
|
||||
:is(#{$flavours}) > :is(.admonition-title, summary) {
|
||||
background-color: color.adjust($tint, $alpha: -0.9);
|
||||
|
||||
// Admonition icon
|
||||
&::before {
|
||||
background-color: $tint;
|
||||
mask-image: var(--md-admonition-icon--#{$name});
|
||||
-webkit-mask-image: var(--md-admonition-icon--#{$name});
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
// Show anchor link on hover over title
|
||||
&:hover a.admonition-anchor-link {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
// During print mode in browser, pdf renderer
|
||||
@media print {
|
||||
// Expand collapsed sections to show details
|
||||
details.admonition::details-content {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Hide details marker for Safari that loves to show it anyway
|
||||
// ref: https://github.com/tommilligan/mdbook-admonish/pull/185
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--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>");
|
||||
}
|
||||
|
||||
// Generate rules for each specified admonition variant
|
||||
@include lib.from-admonitions($admonitions);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Rules: themes
|
||||
// ----------------------------------------------------------------------------
|
||||
//
|
||||
// One rule per builtin theme in mdbook, overriding the default fg/bg if matched
|
||||
//
|
||||
// This must be after the variant CSS, so it can override they styles set there
|
||||
|
||||
.navy {
|
||||
& :is(.admonition) {
|
||||
@@ -215,7 +260,8 @@ a.admonition-anchor-link {
|
||||
}
|
||||
}
|
||||
|
||||
.ayu, .coal {
|
||||
.ayu,
|
||||
.coal {
|
||||
& :is(.admonition) {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
@@ -226,8 +272,10 @@ a.admonition-anchor-link {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
|
||||
& .admonition-anchor-link {
|
||||
&:link, &:visited {
|
||||
&:link,
|
||||
&:visited {
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
}
|
||||
|
||||
52
compile_assets/scss/lib.scss
Normal file
52
compile_assets/scss/lib.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
@use "sass:color";
|
||||
@use "sass:list";
|
||||
|
||||
@mixin from-admonitions($admonitions) {
|
||||
// ----------------------------------------------------------------------------
|
||||
// Rules: layout
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// 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)}");
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Rules: flavours
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@each $names, $props in $admonitions {
|
||||
$name: list.nth($names, 1);
|
||||
$tint: list.nth($props, 1);
|
||||
|
||||
// Admonition flavour selectors
|
||||
$flavours: ();
|
||||
|
||||
@each $name in $names {
|
||||
$flavours: list.join($flavours, ".#{$name}", $separator: comma);
|
||||
}
|
||||
|
||||
// Admonition flavour
|
||||
:is(.admonition):is(#{$flavours}) {
|
||||
border-color: $tint;
|
||||
}
|
||||
|
||||
// Admonition flavour title
|
||||
:is(#{$flavours}) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: color.adjust($tint, $alpha: -0.9);
|
||||
|
||||
// Admonition icon
|
||||
&::before {
|
||||
background-color: $tint;
|
||||
mask-image: var(--md-admonition-icon--#{$name});
|
||||
-webkit-mask-image: var(--md-admonition-icon--#{$name});
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
16
compile_assets/scss/mdbook-admonish-custom-expected.scss
Normal file
16
compile_assets/scss/mdbook-admonish-custom-expected.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
// This file aims to generate the subset of CSS specific to a single admonition directive.
|
||||
//
|
||||
// This is used for unit test data in the rust css generation module.
|
||||
|
||||
@use "sass:color";
|
||||
@use "sass:list";
|
||||
@use "./lib";
|
||||
@import "./material-color";
|
||||
|
||||
$admonitions: (
|
||||
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>",
|
||||
) !default;
|
||||
|
||||
// Generate rules for each specified admonition variant
|
||||
@include lib.from-admonitions($admonitions);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,18 @@ title = "mdbook-admonish-integration"
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "1.0.0" # do not edit: managed by `mdbook-admonish install`
|
||||
assets_version = "3.1.0" # do not edit: managed by `mdbook-admonish install`
|
||||
after = ["links"]
|
||||
|
||||
[[preprocessor.admonish.custom]]
|
||||
directive = "frog"
|
||||
icon = "./frog.svg"
|
||||
color = "#9004CC"
|
||||
|
||||
[preprocessor.admonish.renderer.test]
|
||||
render_mode = "strip"
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["././mdbook-admonish.css"]
|
||||
additional-css = ["./mdbook-admonish.css", "./mdbook-admonish-custom.css"]
|
||||
|
||||
@@ -9,9 +9,18 @@ title = "mdbook-admonish-integration"
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "1.0.0" # do not edit: managed by `mdbook-admonish install`
|
||||
assets_version = "3.1.0" # do not edit: managed by `mdbook-admonish install`
|
||||
after = ["links"]
|
||||
|
||||
[[preprocessor.admonish.custom]]
|
||||
directive = "frog"
|
||||
icon = "./frog.svg"
|
||||
color = "#9004CC"
|
||||
|
||||
[preprocessor.admonish.renderer.test]
|
||||
render_mode = "strip"
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["././mdbook-admonish.css"]
|
||||
additional-css = ["./mdbook-admonish.css", "./mdbook-admonish-custom.css"]
|
||||
|
||||
@@ -1,28 +1,164 @@
|
||||
<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>
|
||||
<div id="admonition-what-is-this" class="admonition abstract">
|
||||
<div id="admonition-what-is-this" class="admonition admonish-abstract" role="note" aria-labelledby="admonition-what-is-this-title">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-what-is-this">
|
||||
<div id="admonition-what-is-this-title">
|
||||
<p>What <i>is</i> this?</p>
|
||||
</a>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-what-is-this"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>This book acts as an integration test for <code>mdbook-admonish</code>.</p>
|
||||
<p>It verifies that <code>mdbook</code> post-processes our generated HTML in the way we expect.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-note" class="admonition note">
|
||||
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-note">
|
||||
<div id="admonition-note-title">
|
||||
<p>Note</p>
|
||||
</a>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>Simples</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-default" class="admonition warning">
|
||||
<div id="admonition-frog" class="admonition admonish-frog" role="note" aria-labelledby="admonition-frog-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-frog-title">
|
||||
<p>Frog</p>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-frog"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>Custom frog directive</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-default" class="admonition admonish-warning" role="note">
|
||||
<div>
|
||||
<p>No title, only body</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-error-rendering-admonishment" class="admonition admonish-bug" role="note" aria-labelledby="admonition-error-rendering-admonishment-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-error-rendering-admonishment-title">
|
||||
<p>Error rendering admonishment</p>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>Failed with:</p>
|
||||
<pre><code class="language-log">'title="' is not a valid directive or TOML key-value pair.
|
||||
|
||||
TOML parsing error: TOML parse error at line 1, column 21
|
||||
|
|
||||
1 | config = { title=" }
|
||||
| ^
|
||||
invalid basic string
|
||||
|
||||
</code></pre>
|
||||
<p>Original markdown input:</p>
|
||||
<pre><code class="language-markdown">```admonish title="
|
||||
No title, only body
|
||||
```
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<details id="admonition-note-1" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-1-title">
|
||||
<summary class="admonition-title">
|
||||
<div id="admonition-note-1-title">
|
||||
<p>Note</p>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note-1"></a>
|
||||
</summary>
|
||||
<div>
|
||||
<p>Hidden on load</p>
|
||||
</div>
|
||||
</details>
|
||||
<div id="admonition-warning" class="admonition admonish-warning" role="note" aria-labelledby="admonition-warning-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-warning-title">
|
||||
<p>Warning</p>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-warning"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>This is a commonly shared warning!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-note-2" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-2-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-note-2-title">
|
||||
<p>Note</p>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note-2"></a>
|
||||
</div>
|
||||
<div>
|
||||
<pre><code class="language-bash">Nested code block
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-note-3" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-3-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-note-3-title">
|
||||
<p>Note</p>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note-3"></a>
|
||||
</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" role="note" aria-labelledby="admonition-note-4-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-note-4-title">
|
||||
<p>Note</p>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note-4"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>Thing two</p>
|
||||
</div>
|
||||
</div>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Thing two nested</p>
|
||||
<div id="admonition-note-5" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-5-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-note-5-title">
|
||||
<p>Note</p>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note-5"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>Thing two nested (should not be a code block)
|
||||
Regression tests for https://github.com/tommilligan/mdbook-admonish/issues/223</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>
|
||||
<p>Thing three</p>
|
||||
<pre><code class="language-sh">Thing three
|
||||
</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
|
||||
20
integration/expected/mdbook-admonish-custom.css
Normal file
20
integration/expected/mdbook-admonish-custom.css
Normal file
@@ -0,0 +1,20 @@
|
||||
:root {
|
||||
--md-admonition-icon--admonish-frog: url("data:image/svg+xml;charset=utf-8,<svg height='800px' width='800px' version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' xml:space='preserve'><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><!-- ref: https://www.svgrepo.com/svg/232692/frog --><!-- Used under CC0 Licence --><path d='M472,268.109H40c-4.418,0-8,3.582-8,8s3.582,8,8,8h432c4.418,0,8-3.582,8-8S476.418,268.109,472,268.109z'/><path d='M320,300.109H192c-4.418,0-8,3.582-8,8s3.582,8,8,8h128c4.418,0,8-3.582,8-8S324.418,300.109,320,300.109z'/><path d='M483.105,224.949c-9.717-8.094-13.741-11.93-11.921-21.398c5.588-10.328,8.816-22.464,8.816-35.441c0-21.712-9.036-41.065-23.058-53.523c-8.67-14.834-24.283-25.463-43.124-29.246c-20.05-4.027-40.644,0.458-55.09,11.994C321.682,126.917,290,140.109,256,140.109s-65.682-13.192-102.728-42.776c-14.446-11.535-35.04-16.02-55.09-11.994c-18.841,3.783-34.454,14.412-43.124,29.246C41.036,127.044,32,146.397,32,168.109c0,12.977,3.228,25.113,8.816,35.441c1.82,9.468-2.204,13.305-11.921,21.398C16.701,235.106,0,249.018,0,284.109c0,30.939,44.4,68.017,85.951,92.4c55.883,32.792,117.864,51.6,170.049,51.6s114.166-18.808,170.049-51.6C467.6,352.126,512,315.049,512,284.109C512,249.018,495.299,235.106,483.105,224.949z M464,168.109c0,10.585-2.696,20.437-7.312,28.661c-0.078,0.131-0.148,0.266-0.219,0.402c-7.916,13.829-21.31,22.937-36.47,22.937c-24.262,0-44-23.327-44-52s19.738-52,44-52c9.282,0,17.896,3.423,25.002,9.245c0.351,0.356,0.731,0.674,1.135,0.955C456.965,135.79,464,150.995,464,168.109z M65.857,126.314c0.407-0.282,0.791-0.603,1.144-0.962c7.105-5.82,15.718-9.242,24.999-9.242c24.262,0,44,23.327,44,52s-19.738,52-44,52c-15.159,0-28.552-9.107-36.468-22.935c-0.072-0.138-0.143-0.275-0.222-0.407C50.696,188.545,48,178.694,48,168.11C48,150.999,55.032,135.795,65.857,126.314z M417.951,362.709c-52.713,30.933-113.256,49.4-161.951,49.4s-109.238-18.467-161.951-49.4C48.095,335.744,16,303.422,16,284.109c0-27.596,11.761-37.393,23.135-46.867c5.547-4.621,11.494-9.583,15.006-16.426c10.334,9.552,23.514,15.293,37.858,15.293c33.084,0,60-30.505,60-68c0-33.803-21.879-61.914-50.429-67.125c15.396-3.025,30.975,0.273,41.717,8.852c40.075,32.002,74.837,46.273,112.712,46.273s72.637-14.271,112.712-46.273c10.743-8.579,26.321-11.878,41.717-8.852C381.879,106.195,360,134.307,360,168.109c0,37.495,26.916,68,60,68c14.344,0,27.524-5.741,37.858-15.293c3.512,6.842,9.459,11.805,15.006,16.426C484.239,246.717,496,256.513,496,284.109C496,303.422,463.905,335.744,417.951,362.709z'/><path d='M92,180.109c8.445,0,36-1.154,36-16s-27.555-16-36-16s-36,1.154-36,16S83.555,180.109,92,180.109z'/><path d='M384,164.109c0,14.846,27.555,16,36,16s36-1.154,36-16s-27.555-16-36-16S384,149.263,384,164.109z'/><path d='M232,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C224,224.528,227.582,228.109,232,228.109z'/><path d='M280,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C272,224.528,275.582,228.109,280,228.109z'/></svg>");
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-frog) {
|
||||
border-color: #9004cc;
|
||||
}
|
||||
|
||||
:is(.admonish-frog) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(144, 4, 204, 0.1);
|
||||
}
|
||||
:is(.admonish-frog) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #9004cc;
|
||||
mask-image: var(--md-admonition-icon--admonish-frog);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-frog);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
29
integration/frog.svg
Normal file
29
integration/frog.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!-- ref: https://www.svgrepo.com/svg/232692/frog -->
|
||||
<!-- Used under CC0 Licence -->
|
||||
<path d="M472,268.109H40c-4.418,0-8,3.582-8,8s3.582,8,8,8h432c4.418,0,8-3.582,8-8S476.418,268.109,472,268.109z"/>
|
||||
<path d="M320,300.109H192c-4.418,0-8,3.582-8,8s3.582,8,8,8h128c4.418,0,8-3.582,8-8S324.418,300.109,320,300.109z"/>
|
||||
<path d="M483.105,224.949c-9.717-8.094-13.741-11.93-11.921-21.398c5.588-10.328,8.816-22.464,8.816-35.441
|
||||
c0-21.712-9.036-41.065-23.058-53.523c-8.67-14.834-24.283-25.463-43.124-29.246c-20.05-4.027-40.644,0.458-55.09,11.994
|
||||
C321.682,126.917,290,140.109,256,140.109s-65.682-13.192-102.728-42.776c-14.446-11.535-35.04-16.02-55.09-11.994
|
||||
c-18.841,3.783-34.454,14.412-43.124,29.246C41.036,127.044,32,146.397,32,168.109c0,12.977,3.228,25.113,8.816,35.441
|
||||
c1.82,9.468-2.204,13.305-11.921,21.398C16.701,235.106,0,249.018,0,284.109c0,30.939,44.4,68.017,85.951,92.4
|
||||
c55.883,32.792,117.864,51.6,170.049,51.6s114.166-18.808,170.049-51.6C467.6,352.126,512,315.049,512,284.109
|
||||
C512,249.018,495.299,235.106,483.105,224.949z M464,168.109c0,10.585-2.696,20.437-7.312,28.661
|
||||
c-0.078,0.131-0.148,0.266-0.219,0.402c-7.916,13.829-21.31,22.937-36.47,22.937c-24.262,0-44-23.327-44-52s19.738-52,44-52
|
||||
c9.282,0,17.896,3.423,25.002,9.245c0.351,0.356,0.731,0.674,1.135,0.955C456.965,135.79,464,150.995,464,168.109z M65.857,126.314
|
||||
c0.407-0.282,0.791-0.603,1.144-0.962c7.105-5.82,15.718-9.242,24.999-9.242c24.262,0,44,23.327,44,52s-19.738,52-44,52
|
||||
c-15.159,0-28.552-9.107-36.468-22.935c-0.072-0.138-0.143-0.275-0.222-0.407C50.696,188.545,48,178.694,48,168.11
|
||||
C48,150.999,55.032,135.795,65.857,126.314z M417.951,362.709c-52.713,30.933-113.256,49.4-161.951,49.4
|
||||
s-109.238-18.467-161.951-49.4C48.095,335.744,16,303.422,16,284.109c0-27.596,11.761-37.393,23.135-46.867
|
||||
c5.547-4.621,11.494-9.583,15.006-16.426c10.334,9.552,23.514,15.293,37.858,15.293c33.084,0,60-30.505,60-68
|
||||
c0-33.803-21.879-61.914-50.429-67.125c15.396-3.025,30.975,0.273,41.717,8.852c40.075,32.002,74.837,46.273,112.712,46.273
|
||||
s72.637-14.271,112.712-46.273c10.743-8.579,26.321-11.878,41.717-8.852C381.879,106.195,360,134.307,360,168.109
|
||||
c0,37.495,26.916,68,60,68c14.344,0,27.524-5.741,37.858-15.293c3.512,6.842,9.459,11.805,15.006,16.426
|
||||
C484.239,246.717,496,256.513,496,284.109C496,303.422,463.905,335.744,417.951,362.709z"/>
|
||||
<path d="M92,180.109c8.445,0,36-1.154,36-16s-27.555-16-36-16s-36,1.154-36,16S83.555,180.109,92,180.109z"/>
|
||||
<path d="M384,164.109c0,14.846,27.555,16,36,16s36-1.154,36-16s-27.555-16-36-16S384,149.263,384,164.109z"/>
|
||||
<path d="M232,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C224,224.528,227.582,228.109,232,228.109z"/>
|
||||
<path d="M280,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C272,224.528,275.582,228.109,280,228.109z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
20
integration/mdbook-admonish-custom.css
Normal file
20
integration/mdbook-admonish-custom.css
Normal file
@@ -0,0 +1,20 @@
|
||||
:root {
|
||||
--md-admonition-icon--admonish-frog: url("data:image/svg+xml;charset=utf-8,<svg height='800px' width='800px' version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' xml:space='preserve'><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><!-- ref: https://www.svgrepo.com/svg/232692/frog --><!-- Used under CC0 Licence --><path d='M472,268.109H40c-4.418,0-8,3.582-8,8s3.582,8,8,8h432c4.418,0,8-3.582,8-8S476.418,268.109,472,268.109z'/><path d='M320,300.109H192c-4.418,0-8,3.582-8,8s3.582,8,8,8h128c4.418,0,8-3.582,8-8S324.418,300.109,320,300.109z'/><path d='M483.105,224.949c-9.717-8.094-13.741-11.93-11.921-21.398c5.588-10.328,8.816-22.464,8.816-35.441c0-21.712-9.036-41.065-23.058-53.523c-8.67-14.834-24.283-25.463-43.124-29.246c-20.05-4.027-40.644,0.458-55.09,11.994C321.682,126.917,290,140.109,256,140.109s-65.682-13.192-102.728-42.776c-14.446-11.535-35.04-16.02-55.09-11.994c-18.841,3.783-34.454,14.412-43.124,29.246C41.036,127.044,32,146.397,32,168.109c0,12.977,3.228,25.113,8.816,35.441c1.82,9.468-2.204,13.305-11.921,21.398C16.701,235.106,0,249.018,0,284.109c0,30.939,44.4,68.017,85.951,92.4c55.883,32.792,117.864,51.6,170.049,51.6s114.166-18.808,170.049-51.6C467.6,352.126,512,315.049,512,284.109C512,249.018,495.299,235.106,483.105,224.949z M464,168.109c0,10.585-2.696,20.437-7.312,28.661c-0.078,0.131-0.148,0.266-0.219,0.402c-7.916,13.829-21.31,22.937-36.47,22.937c-24.262,0-44-23.327-44-52s19.738-52,44-52c9.282,0,17.896,3.423,25.002,9.245c0.351,0.356,0.731,0.674,1.135,0.955C456.965,135.79,464,150.995,464,168.109z M65.857,126.314c0.407-0.282,0.791-0.603,1.144-0.962c7.105-5.82,15.718-9.242,24.999-9.242c24.262,0,44,23.327,44,52s-19.738,52-44,52c-15.159,0-28.552-9.107-36.468-22.935c-0.072-0.138-0.143-0.275-0.222-0.407C50.696,188.545,48,178.694,48,168.11C48,150.999,55.032,135.795,65.857,126.314z M417.951,362.709c-52.713,30.933-113.256,49.4-161.951,49.4s-109.238-18.467-161.951-49.4C48.095,335.744,16,303.422,16,284.109c0-27.596,11.761-37.393,23.135-46.867c5.547-4.621,11.494-9.583,15.006-16.426c10.334,9.552,23.514,15.293,37.858,15.293c33.084,0,60-30.505,60-68c0-33.803-21.879-61.914-50.429-67.125c15.396-3.025,30.975,0.273,41.717,8.852c40.075,32.002,74.837,46.273,112.712,46.273s72.637-14.271,112.712-46.273c10.743-8.579,26.321-11.878,41.717-8.852C381.879,106.195,360,134.307,360,168.109c0,37.495,26.916,68,60,68c14.344,0,27.524-5.741,37.858-15.293c3.512,6.842,9.459,11.805,15.006,16.426C484.239,246.717,496,256.513,496,284.109C496,303.422,463.905,335.744,417.951,362.709z'/><path d='M92,180.109c8.445,0,36-1.154,36-16s-27.555-16-36-16s-36,1.154-36,16S83.555,180.109,92,180.109z'/><path d='M384,164.109c0,14.846,27.555,16,36,16s36-1.154,36-16s-27.555-16-36-16S384,149.263,384,164.109z'/><path d='M232,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C224,224.528,227.582,228.109,232,228.109z'/><path d='M280,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C272,224.528,275.582,228.109,280,228.109z'/></svg>");
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-frog) {
|
||||
border-color: #9004cc;
|
||||
}
|
||||
|
||||
:is(.admonish-frog) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(144, 4, 204, 0.1);
|
||||
}
|
||||
:is(.admonish-frog) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #9004cc;
|
||||
mask-image: var(--md-admonition-icon--admonish-frog);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-frog);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
@@ -34,6 +34,27 @@ if [ "$DIFF_RESULT" != 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eprintln "Generating custom CSS (book)"
|
||||
mdbook-admonish generate-custom mdbook-admonish-custom.css
|
||||
|
||||
eprintln "Verifying generated book custom css"
|
||||
set +e
|
||||
diff -u \
|
||||
"expected/mdbook-admonish-custom.css" \
|
||||
"./mdbook-admonish-custom.css"
|
||||
DIFF_RESULT=$?
|
||||
set -e
|
||||
|
||||
if [ "$DIFF_RESULT" != 0 ]; then
|
||||
eprintln ""
|
||||
eprintln "error: generated custom css was different than expected"
|
||||
eprintln ""
|
||||
eprintln "error: If you expected the output to change, run:"
|
||||
eprintln "cp ./integration/mdbook-admonish-custom.css ./integration/expected/mdbook-admonish-custom.css"
|
||||
eprintln "and commit the result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eprintln "Building mdbook"
|
||||
mdbook build
|
||||
|
||||
@@ -51,7 +72,21 @@ if [ "$DIFF_RESULT" != 0 ]; then
|
||||
eprintln "error: generated html was different than expected"
|
||||
eprintln ""
|
||||
eprintln "error: If you expected the output to change, run:"
|
||||
eprintln "./integration/update-snapshot"
|
||||
eprintln "./integration/scripts/update-snapshot"
|
||||
eprintln "and commit the result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eprintln "Verifying mdbook test runs doctests"
|
||||
set +e
|
||||
TEST_RESULT="$(mdbook test 2>&1 | grep "1 passed; 1 failed")"
|
||||
set -e
|
||||
|
||||
if [[ "$TEST_RESULT" != "test result: FAILED. 1 passed; 1 failed;"* ]]; then
|
||||
eprintln ""
|
||||
eprintln "error: mdbook test did not complete as expected"
|
||||
eprintln ""
|
||||
eprintln "Full output:"
|
||||
mdbook test
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -10,6 +10,65 @@ It verifies that `mdbook` post-processes our generated HTML in the way we expect
|
||||
Simples
|
||||
```
|
||||
|
||||
```admonish frog
|
||||
Custom frog directive
|
||||
```
|
||||
|
||||
```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 two nested
|
||||
|
||||
```admonish
|
||||
Thing two nested (should not be a code block)
|
||||
Regression tests for https://github.com/tommilligan/mdbook-admonish/issues/223
|
||||
```
|
||||
|
||||
1. Thing three
|
||||
|
||||
```sh
|
||||
Thing three
|
||||
```
|
||||
|
||||
3
integration/src/common_warning.md
Normal file
3
integration/src/common_warning.md
Normal file
@@ -0,0 +1,3 @@
|
||||
```admonish warning
|
||||
This is a commonly shared warning!
|
||||
```
|
||||
14
qvet.yml
Normal file
14
qvet.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
action:
|
||||
ready:
|
||||
type: link
|
||||
name: "Push New Version Tag"
|
||||
url: "https://github.com/tommilligan/mdbook-admonish"
|
||||
|
||||
commit:
|
||||
ignore:
|
||||
merges: true
|
||||
|
||||
release:
|
||||
identifiers:
|
||||
- type: tag
|
||||
pattern: "^v[0-9]"
|
||||
24
scripts/build-book
Executable file
24
scripts/build-book
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build book, using styles present in the repository.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
function eprintln() {
|
||||
>&2 echo "$1"
|
||||
}
|
||||
|
||||
eprintln "Installing mdbook-admonish (to system)"
|
||||
cargo install --path . --force
|
||||
|
||||
pushd book
|
||||
eprintln "Installing mdbook-admonish (to book)"
|
||||
mdbook-admonish install .
|
||||
|
||||
eprintln "Building book"
|
||||
mdbook build
|
||||
popd
|
||||
|
||||
eprintln "Book generated at ./book/book/index.html"
|
||||
@@ -8,19 +8,37 @@ 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)"
|
||||
set +e
|
||||
diff -u <(printf "%s" "$COMITTED_ASSETS") <(printf "%s" "$RECOMPILED_ASSETS")
|
||||
DIFF_RESULT=$?
|
||||
set -e
|
||||
|
||||
if [ "$DIFF_RESULT" != 0 ]; then
|
||||
eprintln ""
|
||||
eprintln "error: committed assets are not up to date"
|
||||
eprintln ""
|
||||
eprintln "error: To update committed assets, please run"
|
||||
eprintln "cd compile_assets && yarn run build"
|
||||
eprintln "and commit the result"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
popd > /dev/null
|
||||
|
||||
# Rust things
|
||||
eprintln "Formatting sources"
|
||||
cargo fmt -- --check
|
||||
|
||||
# Known issues:
|
||||
# - RUSTSEC-2020-0071 known unlikely segfault in `time`
|
||||
# - RUSTSEC-2020-0016 `net2` is unmaintained
|
||||
# - RUSTSEC-2020-0159 known unlikely segfault in `chrono`
|
||||
eprintln "Auditing dependencies"
|
||||
cargo audit --deny warnings \
|
||||
--ignore RUSTSEC-2020-0071 \
|
||||
--ignore RUSTSEC-2020-0016 \
|
||||
--ignore RUSTSEC-2020-0159
|
||||
|
||||
eprintln "Linting sources"
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
|
||||
@@ -34,5 +52,6 @@ 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
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -exuo pipefail
|
||||
# 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
|
||||
|
||||
if ! cargo audit --version; then
|
||||
cargo install cargo-audit --force
|
||||
fi
|
||||
if ! mdbook --version; then
|
||||
cargo install mdbook --force
|
||||
fi
|
||||
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
18
scripts/install-mdbook
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
function eprintln() {
|
||||
>&2 echo "$1"
|
||||
}
|
||||
|
||||
VERSION="0.4.51"
|
||||
|
||||
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"
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Development only. Rebuilds the book, including recompiling styles.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
@@ -13,15 +15,4 @@ pushd compile_assets
|
||||
yarn run build
|
||||
popd
|
||||
|
||||
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"
|
||||
./scripts/build-book
|
||||
|
||||
@@ -1 +1 @@
|
||||
^1.0.0
|
||||
^3.0.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.0
|
||||
3.1.0
|
||||
|
||||
@@ -1,30 +1,4 @@
|
||||
: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>");
|
||||
}
|
||||
|
||||
@charset "UTF-8";
|
||||
:is(.admonition) {
|
||||
display: flow-root;
|
||||
margin: 1.5625em 0;
|
||||
@@ -56,36 +30,50 @@ html :is(.admonition) > :last-child {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
a.admonition-anchor-link {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: -1.2rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
a.admonition-anchor-link:link, a.admonition-anchor-link:visited {
|
||||
color: var(--fg);
|
||||
}
|
||||
a.admonition-anchor-link:link:hover, a.admonition-anchor-link:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
a.admonition-anchor-link::before {
|
||||
content: "§";
|
||||
}
|
||||
|
||||
:is(.admonition-title, summary) {
|
||||
: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: 4.4rem 1.2rem;
|
||||
font-weight: 700;
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
display: flex;
|
||||
}
|
||||
:is(.admonition-title, summary) p {
|
||||
:is(.admonition-title, summary.admonition-title) p {
|
||||
margin: 0;
|
||||
}
|
||||
html :is(.admonition-title, summary):last-child {
|
||||
html :is(.admonition-title, summary.admonition-title):last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:is(.admonition-title, summary)::before {
|
||||
:is(.admonition-title, summary.admonition-title)::before {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-start: 1.6rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: #448aff;
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
|
||||
-webkit-mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
|
||||
mask-repeat: no-repeat;
|
||||
@@ -94,205 +82,256 @@ 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) {
|
||||
@media print {
|
||||
details.admonition::details-content {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
summary.admonition-title::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--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>");
|
||||
}
|
||||
|
||||
:root {
|
||||
--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>");
|
||||
}
|
||||
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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);
|
||||
}
|
||||
: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;
|
||||
@@ -303,7 +342,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +1,85 @@
|
||||
use clap::{crate_version, Arg, ArgMatches, Command};
|
||||
use mdbook::{
|
||||
errors::Error,
|
||||
preprocess::{CmdPreprocessor, Preprocessor},
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
|
||||
use mdbook_admonish::Admonish;
|
||||
use std::{io, process};
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::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>,
|
||||
},
|
||||
|
||||
/// Generate CSS file for custom directives.
|
||||
GenerateCustom {
|
||||
/// Root directory for the book, should contain the configuration file (`book.toml`)
|
||||
///
|
||||
/// If not set, defaults to the current directory.
|
||||
#[arg(long)]
|
||||
dir: Option<PathBuf>,
|
||||
|
||||
/// File to write generated css to.
|
||||
output: 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 handle_preprocessing() -> Result<(), Error> {
|
||||
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(".")),
|
||||
),
|
||||
Some(Commands::GenerateCustom { dir, output }) => {
|
||||
handle_generate_custom(dir.unwrap_or_else(|| PathBuf::from(".")), output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_preprocessing() -> std::result::Result<(), mdbook::errors::Error> {
|
||||
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
||||
|
||||
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
|
||||
@@ -71,9 +97,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 {
|
||||
@@ -83,16 +108,59 @@ fn handle_supports(sub_args: &ArgMatches) -> ! {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Config {
|
||||
#[serde(default)]
|
||||
preprocessor: Preprocessors,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
struct Preprocessors {
|
||||
#[serde(default)]
|
||||
admonish: Option<toml::Table>,
|
||||
|
||||
#[serde(flatten)]
|
||||
_other: toml::Table,
|
||||
}
|
||||
|
||||
/// Load the plugin specific config as a toml string, for private deserialization.
|
||||
fn admonish_config_string(config: &Config) -> Result<String> {
|
||||
Ok(toml::to_string(
|
||||
&config
|
||||
.preprocessor
|
||||
.admonish
|
||||
.as_ref()
|
||||
.context("No configuration for mdbook-admonish in book.toml")?,
|
||||
)?)
|
||||
}
|
||||
|
||||
fn handle_generate_custom(proj_dir: PathBuf, output: PathBuf) -> Result<()> {
|
||||
let config = proj_dir.join("book.toml");
|
||||
log::info!("Reading configuration file '{}'", config.display());
|
||||
let data = fs::read_to_string(&config)
|
||||
.with_context(|| format!("can't read configuration file '{}'", config.display()))?;
|
||||
let config: Config = toml::from_str(&data).context("Invalid configuration file")?;
|
||||
|
||||
let css =
|
||||
mdbook_admonish::custom::css_from_config(&proj_dir, &admonish_config_string(&config)?)?;
|
||||
|
||||
log::info!("Writing custom CSS file '{}'", output.display());
|
||||
fs::write(output, css)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-install")]
|
||||
mod install {
|
||||
use clap::ArgMatches;
|
||||
use anyhow::{Context, Result};
|
||||
use path_slash::PathExt;
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
process,
|
||||
};
|
||||
use toml_edit::{self, Array, Document, Item, Table, Value};
|
||||
use toml_edit::{self, Array, DocumentMut, Item, Table, Value};
|
||||
|
||||
const ADMONISH_CSS_FILES: &[(&str, &[u8])] = &[(
|
||||
"mdbook-admonish.css",
|
||||
@@ -112,22 +180,24 @@ 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);
|
||||
// Normalize path to UNIX style.
|
||||
// This avoids conflicts/rewriting files when projects are used under different
|
||||
// operating systems (e.g. on Windows, after being used on Linux)
|
||||
//
|
||||
// https://github.com/tommilligan/mdbook-admonish/issues/161
|
||||
fn normalize_config_file_path(path: &Path) -> Result<Cow<'_, str>> {
|
||||
path.to_slash()
|
||||
.context("UNIX style path normalization error")
|
||||
}
|
||||
|
||||
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");
|
||||
.parse::<DocumentMut>()
|
||||
.context("configuration is not valid TOML")?;
|
||||
|
||||
if let Ok(preprocessor) = preprocessor(&mut doc) {
|
||||
const ASSETS_VERSION: &str = std::include_str!("./assets/VERSION");
|
||||
@@ -137,18 +207,22 @@ mod install {
|
||||
);
|
||||
preprocessor["assets_version"] = value;
|
||||
} else {
|
||||
log::info!("Unexpected configuration, not updating prereprocessor configuration");
|
||||
log::info!("Unexpected configuration, not updating preprocessor 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();
|
||||
|
||||
if let Ok(ref mut additional_css) = additional_css {
|
||||
if !additional_css.contains_str(filepath_str) {
|
||||
let filepath_str = normalize_config_file_path(&filepath)?;
|
||||
|
||||
if !additional_css.contains_str(&filepath_str) {
|
||||
log::info!("Adding '{filepath_str}' to 'additional-css'");
|
||||
additional_css.push(filepath_str);
|
||||
additional_css.push(filepath_str.as_ref());
|
||||
}
|
||||
} else {
|
||||
log::warn!("Unexpected configuration, not updating 'additional-css'");
|
||||
@@ -158,18 +232,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());
|
||||
}
|
||||
@@ -179,14 +253,13 @@ 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(doc: &mut Document) -> Result<&mut Array, ()> {
|
||||
fn additional_css(doc: &mut DocumentMut) -> Result<&mut Array, ()> {
|
||||
let doc = doc.as_table_mut();
|
||||
|
||||
let empty_table = Item::Table(Table::default());
|
||||
@@ -210,7 +283,7 @@ A beautifully styled message.
|
||||
/// Return the preprocessor table for admonish, initializing if required.
|
||||
///
|
||||
/// Return `Err` if the existing configuration is unknown.
|
||||
fn preprocessor(doc: &mut Document) -> Result<&mut Item, ()> {
|
||||
fn preprocessor(doc: &mut DocumentMut) -> Result<&mut Item, ()> {
|
||||
let doc = doc.as_table_mut();
|
||||
|
||||
let empty_table = Item::Table(Table::default());
|
||||
@@ -223,4 +296,24 @@ A beautifully styled message.
|
||||
item["command"] = toml_edit::value("mdbook-admonish");
|
||||
Ok(item)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
/// This test seems redundant, but would fail on Windows.
|
||||
///
|
||||
/// We want to always convert to a fixed output string style, independant
|
||||
/// of runtime platform, and forward slashes in relative paths are fine on
|
||||
/// Windows.
|
||||
#[test]
|
||||
fn test_normalize_config_file_path() {
|
||||
let input = PathBuf::from(".")
|
||||
.join("css-dir")
|
||||
.join("mdbook-admonish.css");
|
||||
let expected = "./css-dir/mdbook-admonish.css";
|
||||
let actual = normalize_config_file_path(&input).unwrap();
|
||||
assert_eq!(actual.as_ref(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
325
src/book_config.rs
Normal file
325
src/book_config.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook::preprocess::PreprocessorContext;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::types::{AdmonitionDefaults, BuiltinDirective, BuiltinDirectiveConfig};
|
||||
|
||||
/// 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::to_string(
|
||||
ctx.config
|
||||
.get_preprocessor("admonish")
|
||||
.context("No configuration for mdbook-admonish in book.toml")?,
|
||||
)
|
||||
.context("Could not serialize mdbook-admonish config. This is a bug in the toml library.")?;
|
||||
admonish_config_from_str(&table)
|
||||
}
|
||||
|
||||
pub(crate) fn admonish_config_from_str(data: &str) -> Result<Config> {
|
||||
let readonly: ConfigReadonly =
|
||||
toml::from_str(data).context("Invalid mdbook-admonish configuration in book.toml")?;
|
||||
let config = readonly.into();
|
||||
log::debug!("Loaded admonish config: {:?}", config);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// All valid input states including back-compatibility fields.
|
||||
///
|
||||
/// This struct deliberately does not implement Serialize as it never meant to
|
||||
/// be written, only converted to Config.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
|
||||
struct ConfigReadonly {
|
||||
#[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>,
|
||||
|
||||
#[serde(default)]
|
||||
pub custom: Vec<CustomDirectiveReadonly>,
|
||||
|
||||
#[serde(default)]
|
||||
pub builtin: HashMap<BuiltinDirective, BuiltinDirectiveConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub directive: DirectiveConfig,
|
||||
}
|
||||
|
||||
/// The canonical config format, without back-compatibility
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
|
||||
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>,
|
||||
|
||||
#[serde(default)]
|
||||
pub directive: DirectiveConfig,
|
||||
}
|
||||
|
||||
impl From<ConfigReadonly> for Config {
|
||||
fn from(other: ConfigReadonly) -> Self {
|
||||
let ConfigReadonly {
|
||||
on_failure,
|
||||
default,
|
||||
renderer,
|
||||
assets_version,
|
||||
custom,
|
||||
builtin,
|
||||
mut directive,
|
||||
} = other;
|
||||
|
||||
// Merge deprecated config fields into main config object
|
||||
directive.custom.extend(
|
||||
custom
|
||||
.into_iter()
|
||||
.map(|CustomDirectiveReadonly { directive, config }| (directive, config)),
|
||||
);
|
||||
directive.builtin.extend(builtin);
|
||||
|
||||
Self {
|
||||
on_failure,
|
||||
default,
|
||||
renderer,
|
||||
assets_version,
|
||||
directive,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
|
||||
pub(crate) struct DirectiveConfig {
|
||||
#[serde(default)]
|
||||
pub custom: HashMap<String, CustomDirective>,
|
||||
|
||||
#[serde(default)]
|
||||
pub builtin: HashMap<BuiltinDirective, BuiltinDirectiveConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct CustomDirective {
|
||||
/// Path to an SVG file, relative to the book root.
|
||||
pub icon: PathBuf,
|
||||
|
||||
/// Primary color for this directive.
|
||||
pub color: hex_color::HexColor,
|
||||
|
||||
/// Alternative directives the user can specify
|
||||
#[serde(default)]
|
||||
pub aliases: Vec<String>,
|
||||
|
||||
/// Title to use, human readable.
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
|
||||
/// Default collapsible value.
|
||||
#[serde(default)]
|
||||
pub collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct CustomDirectiveReadonly {
|
||||
/// The primary directive. Used for CSS classnames
|
||||
pub directive: String,
|
||||
|
||||
/// Path to an SVG file, relative to the book root.
|
||||
#[serde(flatten)]
|
||||
config: CustomDirective,
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::types::BuiltinDirective;
|
||||
|
||||
#[test]
|
||||
fn empty_config_okay() -> Result<()> {
|
||||
let actual = admonish_config_from_str("")?;
|
||||
let expected = Config::default();
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn css_id_prefix_kebab_case_allowed() -> Result<()> {
|
||||
let expected = Config {
|
||||
default: AdmonitionDefaults {
|
||||
css_id_prefix: Some("flam-".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Snake case okay
|
||||
let actual = admonish_config_from_str(r#"default = { css_id_prefix = "flam-" }"#)?;
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
// Kebab case back-compat okay
|
||||
let actual = admonish_config_from_str(r#"default = { css-id-prefix = "flam-" }"#)?;
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_old_and_new_custom_directives() -> Result<()> {
|
||||
let serialized = r##"
|
||||
[directive.custom.purple]
|
||||
icon = "/tmp/test-directive.svg"
|
||||
color = "#9B4F96"
|
||||
aliases = ["test-directive-alias-0"]
|
||||
title = "Purple"
|
||||
collapsible = true
|
||||
|
||||
[[custom]]
|
||||
directive = "blue"
|
||||
icon = "/tmp/test-directive.svg"
|
||||
color = "#0038A8"
|
||||
aliases = []
|
||||
title = "Blue"
|
||||
"##;
|
||||
let expected = Config {
|
||||
directive: DirectiveConfig {
|
||||
custom: HashMap::from([
|
||||
(
|
||||
"purple".to_owned(),
|
||||
CustomDirective {
|
||||
icon: PathBuf::from("/tmp/test-directive.svg"),
|
||||
color: hex_color::HexColor::from((155, 79, 150)),
|
||||
aliases: vec!["test-directive-alias-0".to_owned()],
|
||||
title: Some("Purple".to_owned()),
|
||||
collapsible: Some(true),
|
||||
},
|
||||
),
|
||||
(
|
||||
"blue".to_owned(),
|
||||
CustomDirective {
|
||||
icon: PathBuf::from("/tmp/test-directive.svg"),
|
||||
color: hex_color::HexColor::from((0, 56, 168)),
|
||||
aliases: vec![],
|
||||
title: Some("Blue".to_owned()),
|
||||
collapsible: None,
|
||||
},
|
||||
),
|
||||
]),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let actual = admonish_config_from_str(serialized)?;
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_config_roundtrip() -> Result<()> {
|
||||
let input = Config {
|
||||
default: AdmonitionDefaults {
|
||||
css_id_prefix: Some("flam-".to_owned()),
|
||||
collapsible: true,
|
||||
title: Some("".to_owned()),
|
||||
},
|
||||
assets_version: Some("1.1.1".to_owned()),
|
||||
directive: DirectiveConfig {
|
||||
custom: HashMap::from([(
|
||||
"test-directive".to_owned(),
|
||||
CustomDirective {
|
||||
icon: PathBuf::from("/tmp/test-directive.svg"),
|
||||
color: hex_color::HexColor::from((155, 79, 150)),
|
||||
aliases: vec!["test-directive-alias-0".to_owned()],
|
||||
title: Some("test-directive-title".to_owned()),
|
||||
collapsible: Some(true),
|
||||
},
|
||||
)]),
|
||||
builtin: HashMap::from([(
|
||||
BuiltinDirective::Warning,
|
||||
BuiltinDirectiveConfig {
|
||||
collapsible: Some(true),
|
||||
},
|
||||
)]),
|
||||
},
|
||||
on_failure: OnFailure::Bail,
|
||||
renderer: HashMap::from([(
|
||||
"test-mode".to_owned(),
|
||||
RendererConfig {
|
||||
render_mode: Some(RenderMode::Strip),
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let expected = r##"on_failure = "bail"
|
||||
assets_version = "1.1.1"
|
||||
|
||||
[default]
|
||||
title = ""
|
||||
collapsible = true
|
||||
css_id_prefix = "flam-"
|
||||
|
||||
[renderer.test-mode]
|
||||
render_mode = "strip"
|
||||
|
||||
[directive.custom.test-directive]
|
||||
icon = "/tmp/test-directive.svg"
|
||||
color = "#9B4F96"
|
||||
aliases = ["test-directive-alias-0"]
|
||||
title = "test-directive-title"
|
||||
collapsible = true
|
||||
|
||||
[directive.builtin.warning]
|
||||
collapsible = true
|
||||
"##;
|
||||
|
||||
let serialized = toml::to_string(&input)?;
|
||||
assert_eq!(serialized, expected);
|
||||
|
||||
let actual = admonish_config_from_str(&serialized)?;
|
||||
assert_eq!(actual, input);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
120
src/config/mod.rs
Normal file
120
src/config/mod.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
mod toml_wrangling;
|
||||
mod v1;
|
||||
mod v2;
|
||||
mod v3;
|
||||
|
||||
/// 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, Default)]
|
||||
pub(crate) struct InstanceConfig {
|
||||
pub(crate) directive: String,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) id: 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)?;
|
||||
Some(Self::from_admonish_config_string(config_string))
|
||||
}
|
||||
|
||||
/// Parse an info string that is known to be for `admonish`.
|
||||
fn from_admonish_config_string(config_string: &str) -> Result<Self, String> {
|
||||
// If we succeed at parsing v3, return that. Otherwise hold onto the error
|
||||
let config_v3_error = match v3::from_config_string(config_string) {
|
||||
Ok(config) => return Ok(config),
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
// If we succeed at parsing v2, return that
|
||||
if let Ok(config) = v2::from_config_string(config_string) {
|
||||
return Ok(config);
|
||||
};
|
||||
|
||||
// If we succeed at parsing v1, return that.
|
||||
if let Ok(config) = v1::from_config_string(config_string) {
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// Otherwise return our v3 error.
|
||||
Err(config_v3_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,
|
||||
id: 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" id="my-id""#
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "question".to_owned(),
|
||||
title: Some("Custom Title".to_owned()),
|
||||
id: Some("my-id".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
// v3 syntax is supported
|
||||
assert_eq!(
|
||||
InstanceConfig::from_info_string(
|
||||
r#"admonish title="Custom Title", type="question", id="my-id""#
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "question".to_owned(),
|
||||
title: Some("Custom Title".to_owned()),
|
||||
id: Some("my-id".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
44
src/config/toml_wrangling.rs
Normal file
44
src/config/toml_wrangling.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub(crate) struct UserInput {
|
||||
#[serde(default)]
|
||||
pub r#type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub class: Option<String>,
|
||||
#[serde(default)]
|
||||
pub collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
impl UserInput {
|
||||
pub fn classnames(&self) -> Vec<String> {
|
||||
self.class
|
||||
.as_ref()
|
||||
.map(|class| {
|
||||
class
|
||||
.split(' ')
|
||||
.filter(|classname| !classname.is_empty())
|
||||
.map(|classname| classname.to_owned())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) static RX_DIRECTIVE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^[A-Za-z0-9_-]+$"#).expect("directive regex"));
|
||||
|
||||
pub(crate) fn format_toml_parsing_error(error: impl Display) -> String {
|
||||
format!("TOML parsing error: {error}")
|
||||
}
|
||||
|
||||
pub(crate) fn format_invalid_directive(directive: &str, original_error: impl Display) -> String {
|
||||
format!("'{directive}' is not a valid directive or TOML key-value pair.\n\n{original_error}")
|
||||
}
|
||||
136
src/config/v1.rs
Normal file
136
src/config/v1.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
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,
|
||||
id: None,
|
||||
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,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(" ").unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("unknown").unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "unknown".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("note").unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string("note.additional-classname").unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: None,
|
||||
id: 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
210
src/config/v2.rs
Normal file
210
src/config/v2.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
use super::toml_wrangling::{
|
||||
format_invalid_directive, format_toml_parsing_error, UserInput, RX_DIRECTIVE,
|
||||
};
|
||||
use super::InstanceConfig;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
/// 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(|| Regex::new(r#"(?:[A-Za-z0-9_-]+) *="#).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()
|
||||
}
|
||||
|
||||
fn user_input_from_config_toml(config_toml: &str) -> Result<UserInput, String> {
|
||||
toml::from_str(config_toml).map_err(format_toml_parsing_error)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// The basic idea here is to accept space separated key-value pairs, break them
|
||||
/// onto separate lines, and then parse them as a TOML document.
|
||||
/// This breaks when values contain a literal '=' sign, for which v3 syntax should be used.
|
||||
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 = 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, ""),
|
||||
};
|
||||
|
||||
if !RX_DIRECTIVE.is_match(directive) {
|
||||
return Err(format_invalid_directive(directive, original_error));
|
||||
}
|
||||
|
||||
let mut config = user_input_from_config_toml(config_toml)?;
|
||||
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,
|
||||
id: config.id,
|
||||
additional_classnames,
|
||||
collapsible: config.collapsible,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_v2() -> Result<(), ()> {
|
||||
fn check(config_string: &str, expected: InstanceConfig) -> Result<(), ()> {
|
||||
let actual = match from_config_string(config_string) {
|
||||
Ok(config) => config,
|
||||
Err(error) => panic!("Expected config to be valid, got error:\n\n{}", error),
|
||||
};
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
check(
|
||||
"",
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
check(
|
||||
" ",
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
check(
|
||||
r#"type="note" class="additional classname" title="Никита" collapsible=true"#,
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: Some("Никита".to_owned()),
|
||||
id: None,
|
||||
additional_classnames: vec!["additional".to_owned(), "classname".to_owned()],
|
||||
collapsible: Some(true),
|
||||
},
|
||||
)?;
|
||||
// Specifying unknown keys is okay, as long as they're valid
|
||||
check(
|
||||
r#"unkonwn="but valid toml""#,
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
// Just directive is fine
|
||||
check(
|
||||
r#"info"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
// Directive plus toml config
|
||||
check(
|
||||
r#"info title="Information" collapsible=false"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("Information".to_owned()),
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: Some(false),
|
||||
},
|
||||
)?;
|
||||
// Test custom id
|
||||
check(
|
||||
r#"info title="My Info" id="my-info-custom-id""#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("My Info".to_owned()),
|
||||
id: Some("my-info-custom-id".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
// Directive after toml config is an error
|
||||
assert!(from_config_string(r#"title="Information" info"#).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_invalid_directive() {
|
||||
assert_eq!(
|
||||
from_config_string(r#"oh!wow titlel=""#).unwrap_err(),
|
||||
r#"'oh!wow' is not a valid directive or TOML key-value pair.
|
||||
|
||||
TOML parsing error: TOML parse error at line 1, column 3
|
||||
|
|
||||
1 | oh!wow
|
||||
| ^
|
||||
expected `.`, `=`
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[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 9
|
||||
|
|
||||
1 | titlel="
|
||||
| ^
|
||||
invalid basic string
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
||||
202
src/config/v3.rs
Normal file
202
src/config/v3.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use super::toml_wrangling::{
|
||||
format_invalid_directive, format_toml_parsing_error, UserInput, RX_DIRECTIVE,
|
||||
};
|
||||
use super::InstanceConfig;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct Wrapper<T> {
|
||||
config: T,
|
||||
}
|
||||
|
||||
/// Transform our config string into valid toml
|
||||
fn bare_inline_table_to_toml(pairs: &str) -> String {
|
||||
format!("config = {{ {pairs} }}")
|
||||
}
|
||||
|
||||
fn user_input_from_config_string(config_string: &str) -> Result<UserInput, String> {
|
||||
match toml::from_str::<Wrapper<_>>(&bare_inline_table_to_toml(config_string)) {
|
||||
Ok(wrapper) => Ok(wrapper.config),
|
||||
Err(error) => Err(format_toml_parsing_error(error)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse and return the config assuming v3 format.
|
||||
///
|
||||
/// Note that if an error occurs, a parsed struct that can be returned to
|
||||
/// show the error message will be returned.
|
||||
///
|
||||
/// The basic idea here is to accept the inside of an inline table, wrap it,
|
||||
/// parse it, and then use the toml values.
|
||||
pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig, String> {
|
||||
let config_string = config_string.trim();
|
||||
|
||||
let config = match user_input_from_config_string(config_string) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
// For ergonomic reasons, we allow users to specify the directive without
|
||||
// a key. So if parsing fails initially, take the first word,
|
||||
// use that as the directive, and reparse.
|
||||
let (directive, config_string) = match config_string.split_once(' ') {
|
||||
Some((directive, config_string)) => (directive.trim(), config_string.trim()),
|
||||
None => (config_string, ""),
|
||||
};
|
||||
|
||||
if !RX_DIRECTIVE.is_match(directive) {
|
||||
return Err(format_invalid_directive(directive, error));
|
||||
}
|
||||
|
||||
let mut config = user_input_from_config_string(config_string)?;
|
||||
config.r#type = Some(directive.to_owned());
|
||||
config
|
||||
}
|
||||
};
|
||||
|
||||
let additional_classnames = config.classnames();
|
||||
Ok(InstanceConfig {
|
||||
directive: config.r#type.unwrap_or_default(),
|
||||
title: config.title,
|
||||
id: config.id,
|
||||
additional_classnames,
|
||||
collapsible: config.collapsible,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_v3() -> Result<(), ()> {
|
||||
fn check(config_string: &str, expected: InstanceConfig) -> Result<(), ()> {
|
||||
let actual = match from_config_string(config_string) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
panic!("Expected config '{config_string}' to be valid, got error:\n\n{error}")
|
||||
}
|
||||
};
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
check(
|
||||
"",
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
check(
|
||||
" ",
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
check(
|
||||
r#"type="note", class="additional classname", title="Никита", collapsible=true"#,
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: Some("Никита".to_owned()),
|
||||
id: None,
|
||||
additional_classnames: vec!["additional".to_owned(), "classname".to_owned()],
|
||||
collapsible: Some(true),
|
||||
},
|
||||
)?;
|
||||
// Specifying unknown keys is okay, as long as they're valid
|
||||
check(
|
||||
r#"unkonwn="but valid toml""#,
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
// Just directive is fine
|
||||
check(
|
||||
r#"info"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
// Directive plus toml config
|
||||
check(
|
||||
r#"info title="Information", collapsible=false"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("Information".to_owned()),
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: Some(false),
|
||||
},
|
||||
)?;
|
||||
// Test custom id
|
||||
check(
|
||||
r#"info title="My Info", id="my-info-custom-id""#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("My Info".to_owned()),
|
||||
id: Some("my-info-custom-id".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
// Directive after toml config is an error
|
||||
assert!(from_config_string(r#"title="Information" info"#).is_err());
|
||||
// HTML with quotes inside content
|
||||
// Note that we use toml literal (single quoted) strings here
|
||||
check(
|
||||
r#"info title='My <span class="emphasis">Title</span>'"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some(r#"My <span class="emphasis">Title</span>"#.to_owned()),
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_invalid_directive() {
|
||||
assert_eq!(
|
||||
from_config_string(r#"oh!wow titlel=""#).unwrap_err(),
|
||||
r#"'oh!wow' is not a valid directive or TOML key-value pair.
|
||||
|
||||
TOML parsing error: TOML parse error at line 1, column 14
|
||||
|
|
||||
1 | config = { oh!wow titlel=" }
|
||||
| ^
|
||||
expected `.`, `=`
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[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 22
|
||||
|
|
||||
1 | config = { titlel=" }
|
||||
| ^
|
||||
invalid basic string
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
||||
111
src/custom.rs
Normal file
111
src/custom.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
//! This module is responsible for generating custom CSS for new admonition variants.
|
||||
//!
|
||||
//! It has unit tests to ensure the output matches that of the compile_assets CSS.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use hex_color::{Case, HexColor};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
static RX_COLLAPSE_NEWLINES: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"[\r\n]+\s*").expect("invalid whitespace regex"));
|
||||
|
||||
// Do some simple things to make the svg input probably a valid data url
|
||||
// Based on this gist: https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
|
||||
fn svg_to_data_url(svg: &str) -> String {
|
||||
const XMLNS: &str = r#"http://www.w3.org/2000/svg"#;
|
||||
//
|
||||
let mut svg = RX_COLLAPSE_NEWLINES.replace_all(svg, "").to_string();
|
||||
if !svg.contains(XMLNS) {
|
||||
log::warn!("Your SVG file does not contain '<svg xmlns=\"{XMLNS}\"', it will likely fail to render.");
|
||||
}
|
||||
|
||||
svg = svg
|
||||
.replace('"', "'")
|
||||
.replace('%', "%25")
|
||||
.replace('#', "%23")
|
||||
.replace('{', "%7B")
|
||||
.replace('}', "%7D");
|
||||
format!("url(\"data:image/svg+xml;charset=utf-8,{}\")", svg)
|
||||
}
|
||||
|
||||
/// Given a valid set of inputs, generate the relevant CSS.
|
||||
///
|
||||
/// It is up to the caller to validate inputs.
|
||||
fn directive_css(name: &str, svg_data: &str, tint: HexColor) -> String {
|
||||
let data_url = svg_to_data_url(svg_data);
|
||||
let tint_faint = format!("rgba({}, {}, {}, {})", tint.r, tint.g, tint.b, 0.1);
|
||||
let tint = tint.display_rgb().with_case(Case::Lower);
|
||||
format!(
|
||||
":root {{
|
||||
--md-admonition-icon--admonish-{name}: {data_url};
|
||||
}}
|
||||
|
||||
:is(.admonition):is(.admonish-{name}) {{
|
||||
border-color: {tint};
|
||||
}}
|
||||
|
||||
:is(.admonish-{name}) > :is(.admonition-title, summary.admonition-title) {{
|
||||
background-color: {tint_faint};
|
||||
}}
|
||||
:is(.admonish-{name}) > :is(.admonition-title, summary.admonition-title)::before {{
|
||||
background-color: {tint};
|
||||
mask-image: var(--md-admonition-icon--admonish-{name});
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-{name});
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}}
|
||||
",
|
||||
name = name,
|
||||
data_url = data_url,
|
||||
tint = tint,
|
||||
tint_faint = tint_faint
|
||||
)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn css_from_config(book_dir: &Path, config: &str) -> Result<String> {
|
||||
let config = crate::book_config::admonish_config_from_str(config)?;
|
||||
let custom_directives = config.directive.custom;
|
||||
|
||||
if custom_directives.is_empty() {
|
||||
return Err(anyhow!("No custom directives provided"));
|
||||
}
|
||||
|
||||
log::info!("Loaded {} custom directives", custom_directives.len());
|
||||
let mut css = String::new();
|
||||
for (directive_name, directive) in custom_directives.iter() {
|
||||
let svg = fs::read_to_string(book_dir.join(&directive.icon))
|
||||
.with_context(|| format!("can't read icon file '{}'", directive.icon.display()))?;
|
||||
css.push_str(&directive_css(directive_name, &svg, directive.color));
|
||||
}
|
||||
Ok(css)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const GENERATED_CSS: &str = include_str!("./test_data/mdbook-admonish-custom-expected.css");
|
||||
const NOTE_SVG: &str = r#"<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>
|
||||
"#;
|
||||
|
||||
// Verify the generated CSS here against a sample from the compile_assets output.
|
||||
//
|
||||
// The ensures that any new custom CSS will be in line with official styles.
|
||||
#[test]
|
||||
fn verify_against_generated_css() {
|
||||
let actual = directive_css("note", NOTE_SVG, HexColor::parse("#448aff").unwrap());
|
||||
assert_eq!(
|
||||
GENERATED_CSS, actual,
|
||||
"Rust generated CSS is out of step with SCSS generated CSS"
|
||||
)
|
||||
}
|
||||
}
|
||||
792
src/lib.rs
792
src/lib.rs
@@ -1,776 +1,16 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use mdbook::{
|
||||
book::{Book, BookItem},
|
||||
errors::Result as MdbookResult,
|
||||
preprocess::{Preprocessor, PreprocessorContext},
|
||||
utils::unique_id_from_content,
|
||||
};
|
||||
use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag};
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
|
||||
pub struct Admonish;
|
||||
|
||||
impl Preprocessor for Admonish {
|
||||
fn name(&self) -> &str {
|
||||
"admonish"
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> MdbookResult<Book> {
|
||||
ensure_compatible_assets_version(ctx)?;
|
||||
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).map(|md| {
|
||||
chapter.content = md;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
res.unwrap_or(Ok(())).map(|_| book)
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
renderer == "html"
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_compatible_assets_version(ctx: &PreprocessorContext) -> 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 ctx
|
||||
.config
|
||||
.get("preprocessor.admonish.assets_version")
|
||||
.and_then(|value| value.as_str())
|
||||
{
|
||||
Some(version) => version,
|
||||
None => {
|
||||
return Err(anyhow!(
|
||||
r#"ERROR:
|
||||
Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but did not find a version.
|
||||
{USER_ACTION}
|
||||
{DOCS_REFERENCE}"#
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let version = Version::parse(version).unwrap();
|
||||
|
||||
if !requirement.matches(&version) {
|
||||
return Err(anyhow!(
|
||||
r#"ERROR:
|
||||
Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but found '{version}'.
|
||||
{USER_ACTION}
|
||||
{DOCS_REFERENCE}"#
|
||||
));
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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 AdmonitionInfoRaw<'a> {
|
||||
directive: &'a str,
|
||||
title: Option<String>,
|
||||
additional_classnames: Option<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct AdmonitionInfo<'a> {
|
||||
directive: Directive,
|
||||
title: Option<String>,
|
||||
additional_classnames: Option<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
impl<'a> From<AdmonitionInfoRaw<'a>> for AdmonitionInfo<'a> {
|
||||
fn from(other: AdmonitionInfoRaw<'a>) -> Self {
|
||||
let AdmonitionInfoRaw {
|
||||
directive: raw_directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
} = other;
|
||||
let (directive, title) = match (Directive::from_str(raw_directive), title) {
|
||||
(Ok(directive), None) => (directive, ucfirst(raw_directive)),
|
||||
(Err(_), None) => (Directive::Note, "Note".to_owned()),
|
||||
(Ok(directive), Some(title)) => (directive, title),
|
||||
(Err(_), Some(title)) => (Directive::Note, title),
|
||||
};
|
||||
// If the user explicitly gave no title, then disable the title bar
|
||||
let title = if title.is_empty() { None } else { Some(title) };
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct Admonition<'a> {
|
||||
directive: Directive,
|
||||
title: Option<String>,
|
||||
content: &'a str,
|
||||
additional_classnames: Option<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
impl<'a> Admonition<'a> {
|
||||
pub fn new(info: AdmonitionInfo<'a>, content: &'a str) -> Self {
|
||||
let AdmonitionInfo {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
} = info;
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
content,
|
||||
additional_classnames,
|
||||
}
|
||||
}
|
||||
|
||||
fn html(&self, anchor_id: &str) -> String {
|
||||
let mut additional_class = Cow::Borrowed(self.directive.classname());
|
||||
let title = &self.title;
|
||||
let content = &self.content;
|
||||
|
||||
let title_html = title
|
||||
.as_ref()
|
||||
.map(|title| {
|
||||
Cow::Owned(format!(
|
||||
r##"<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#{ANCHOR_ID_PREFIX}-{anchor_id}">
|
||||
|
||||
{title}
|
||||
|
||||
</a>
|
||||
</div>
|
||||
"##
|
||||
))
|
||||
})
|
||||
.unwrap_or(Cow::Borrowed(""));
|
||||
|
||||
if let Some(additional_classnames) = &self.additional_classnames {
|
||||
let mut buffer = additional_class.into_owned();
|
||||
for additional_classname in additional_classnames {
|
||||
buffer.push(' ');
|
||||
buffer.push_str(additional_classname);
|
||||
}
|
||||
|
||||
additional_class = Cow::Owned(buffer);
|
||||
}
|
||||
|
||||
// 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#"<div id="{ANCHOR_ID_PREFIX}-{anchor_id}" class="admonition {additional_class}">
|
||||
{title_html}<div>
|
||||
|
||||
{content}
|
||||
|
||||
</div>
|
||||
</div>"#,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ADMONISH_BLOCK_KEYWORD: &str = "admonish";
|
||||
const ANCHOR_ID_PREFIX: &str = "admonition";
|
||||
const ANCHOR_ID_DEFAULT: &str = "default";
|
||||
|
||||
/// Returns:
|
||||
/// - `None` if this is not an `admonish` block.
|
||||
/// - `Some(AdmonitionInfoRaw)` if this is an `admonish` block
|
||||
fn parse_info_string(info_string: &str) -> Option<AdmonitionInfoRaw> {
|
||||
// Get the rest of the info string if this is an admonition
|
||||
let directive_title = if info_string == ADMONISH_BLOCK_KEYWORD {
|
||||
""
|
||||
} else {
|
||||
match info_string.split_once(' ') {
|
||||
Some((ADMONISH_BLOCK_KEYWORD, rest)) => rest,
|
||||
_ => return None,
|
||||
}
|
||||
};
|
||||
|
||||
// If we're just given the directive, handle that
|
||||
let (directive, title) = directive_title
|
||||
.split_once(' ')
|
||||
.map(|(directive, title)| (directive, Some(title)))
|
||||
.unwrap_or_else(|| (directive_title, 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)
|
||||
.unwrap_or_else(|error| format!("Error parsing JSON string: {error}"))
|
||||
});
|
||||
|
||||
// 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, None),
|
||||
Some((directive, additional_classnames)) => (
|
||||
directive,
|
||||
Some(
|
||||
additional_classnames
|
||||
.split(CLASSNAME_SEPARATOR)
|
||||
.filter(|additional_classname| !additional_classname.is_empty())
|
||||
.collect(),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
Some(AdmonitionInfoRaw {
|
||||
directive,
|
||||
title,
|
||||
additional_classnames,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 extract_admonish_body(content: &str) -> &str {
|
||||
const PRE_END: char = '\n';
|
||||
const POST: &str = "```";
|
||||
|
||||
// We can't trust the info string length to find the start of the body
|
||||
// it may change length if it contains HTML or character escapes.
|
||||
//
|
||||
// So we scan for the first newline and use that.
|
||||
// If gods forbid it doesn't exist for some reason, just include the whole info string.
|
||||
let start_index = content
|
||||
// Start one character _after_ the newline
|
||||
.find(PRE_END)
|
||||
.map(|index| index + 1)
|
||||
.unwrap_or_default();
|
||||
let end_index = content.len() - POST.len();
|
||||
|
||||
let admonish_content = &content[start_index..end_index];
|
||||
// The newline after a code block is technically optional, so we have to
|
||||
// trim it off dynamically.
|
||||
admonish_content.trim()
|
||||
}
|
||||
|
||||
/// Given the content in the span of the code block, and the info string,
|
||||
/// return `Some(Admonition)` if the code block is an admonition.
|
||||
///
|
||||
/// If the code block is not an admonition, return `None`.
|
||||
fn parse_admonition<'a>(info_string: &'a str, content: &'a str) -> Option<Admonition<'a>> {
|
||||
let info = parse_info_string(info_string)?;
|
||||
let info = AdmonitionInfo::from(info);
|
||||
let body = extract_admonish_body(content);
|
||||
Some(Admonition::new(info, body))
|
||||
}
|
||||
|
||||
fn preprocess(content: &str) -> 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 (e, span) in events.into_offset_iter() {
|
||||
if let Event::Start(Tag::CodeBlock(Fenced(info_string))) = e.clone() {
|
||||
let span_content = &content[span.start..span.end];
|
||||
let admonition = match parse_admonition(info_string.as_ref(), span_content) {
|
||||
Some(admonition) => admonition,
|
||||
None => continue,
|
||||
};
|
||||
let anchor_id = unique_id_from_content(
|
||||
admonition.title.as_deref().unwrap_or(ANCHOR_ID_DEFAULT),
|
||||
&mut id_counter,
|
||||
);
|
||||
admonish_blocks.push((span, admonition.html(&anchor_id)));
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn prep(content: &str) -> String {
|
||||
preprocess(content).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_info_string() {
|
||||
assert_eq!(parse_info_string(""), None);
|
||||
assert_eq!(parse_info_string("adm"), None);
|
||||
assert_eq!(
|
||||
parse_info_string("admonish"),
|
||||
Some(AdmonitionInfoRaw {
|
||||
directive: "",
|
||||
title: None,
|
||||
additional_classnames: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse_info_string("admonish "),
|
||||
Some(AdmonitionInfoRaw {
|
||||
directive: "",
|
||||
title: None,
|
||||
additional_classnames: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse_info_string("admonish unknown"),
|
||||
Some(AdmonitionInfoRaw {
|
||||
directive: "unknown",
|
||||
title: None,
|
||||
additional_classnames: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse_info_string("admonish note"),
|
||||
Some(AdmonitionInfoRaw {
|
||||
directive: "note",
|
||||
title: None,
|
||||
additional_classnames: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
parse_info_string("admonish note.additional-classname"),
|
||||
Some(AdmonitionInfoRaw {
|
||||
directive: "note",
|
||||
title: None,
|
||||
additional_classnames: Some(vec!["additional-classname"])
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish() {
|
||||
let content = r#"# Chapter
|
||||
```admonish
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-note">
|
||||
|
||||
Note
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive() {
|
||||
let content = r#"# Chapter
|
||||
```admonish warning
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-warning" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-warning">
|
||||
|
||||
Warning
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_admonish_directive_title() {
|
||||
let content = r#"# Chapter
|
||||
```admonish warning "Read **this**!"
|
||||
A simple admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-read-this" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-read-this">
|
||||
|
||||
Read **this**!
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_tables_untouched() {
|
||||
// Regression test.
|
||||
// Previously we forgot to enable the same markdwon extensions as mdbook itself.
|
||||
|
||||
let content = r#"# Heading
|
||||
| Head 1 | Head 2 |
|
||||
|--------|--------|
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
| Head 1 | Head 2 |
|
||||
|--------|--------|
|
||||
| Row 1 | Row 2 |
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_html_untouched() {
|
||||
// Regression test.
|
||||
// Don't remove important newlines for syntax nested inside HTML
|
||||
|
||||
let content = r#"# Heading
|
||||
<del>
|
||||
*foo*
|
||||
</del>
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
<del>
|
||||
*foo*
|
||||
</del>
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_in_list() {
|
||||
// Regression test.
|
||||
// Don't remove important newlines for syntax nested inside HTML
|
||||
|
||||
let content = r#"# Heading
|
||||
1. paragraph 1
|
||||
```
|
||||
code 1
|
||||
```
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
let expected = r#"# Heading
|
||||
1. paragraph 1
|
||||
```
|
||||
code 1
|
||||
```
|
||||
2. paragraph 2
|
||||
"#;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_string_that_changes_length_when_parsed() {
|
||||
let content = r#"
|
||||
```admonish note "And \\"<i>in</i>\\" the title"
|
||||
With <b>html</b> styling.
|
||||
```
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-and-in-the-title" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-and-in-the-title">
|
||||
|
||||
And "<i>in</i>" the title
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
With <b>html</b> styling.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
hello
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_string_ending_in_symbol() {
|
||||
let content = r#"
|
||||
```admonish warning "Trademark™"
|
||||
Should be respected
|
||||
```
|
||||
hello
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-trademark" class="admonition warning">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-trademark">
|
||||
|
||||
Trademark™
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Should be respected
|
||||
|
||||
</div>
|
||||
</div>
|
||||
hello
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_additional_classname() {
|
||||
let content = r#"
|
||||
```admonish tip.my-style.other-style
|
||||
Will have bonus classnames
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-tip" class="admonition tip my-style other-style">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-tip">
|
||||
|
||||
Tip
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Will have bonus classnames
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_additional_classname_and_title() {
|
||||
let content = r#"
|
||||
```admonish tip.my-style.other-style "Developers don't want you to know this one weird tip!"
|
||||
Will have bonus classnames
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-developers-dont-want-you-to-know-this-one-weird-tip" class="admonition tip my-style other-style">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-developers-dont-want-you-to-know-this-one-weird-tip">
|
||||
|
||||
Developers don't want you to know this one weird tip!
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Will have bonus classnames
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_with_empty_additional_classnames_title_content() {
|
||||
let content = r#"
|
||||
```admonish .... ""
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-default" class="admonition note">
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unique_ids_same_title() {
|
||||
let content = r#"
|
||||
```admonish note "My Note"
|
||||
Content zero.
|
||||
```
|
||||
|
||||
```admonish note "My Note"
|
||||
Content one.
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-my-note" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-my-note">
|
||||
|
||||
My Note
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Content zero.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="admonition-my-note-1" class="admonition note">
|
||||
<div class="admonition-title">
|
||||
<a class="admonition-anchor-link" href="#admonition-my-note-1">
|
||||
|
||||
My Note
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Content one.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
}
|
||||
//! For usage and reference, see the [mdbook-admonish book](https://tommilligan.github.io/mdbook-admonish/)
|
||||
//!
|
||||
//! Documentation is hosted externally, as docs.rs does not currently support plugins.
|
||||
|
||||
mod book_config;
|
||||
mod config;
|
||||
#[doc(hidden)]
|
||||
pub mod custom;
|
||||
mod markdown;
|
||||
mod parse;
|
||||
mod preprocessor;
|
||||
mod render;
|
||||
mod resolve;
|
||||
mod types;
|
||||
|
||||
pub use crate::preprocessor::Admonish;
|
||||
|
||||
1264
src/markdown.rs
Normal file
1264
src/markdown.rs
Normal file
File diff suppressed because it is too large
Load Diff
240
src/parse.rs
Normal file
240
src/parse.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
book_config::OnFailure,
|
||||
render::Admonition,
|
||||
resolve::AdmonitionMeta,
|
||||
types::{BuiltinDirective, CssId, Overrides},
|
||||
};
|
||||
|
||||
/// 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,
|
||||
overrides: &'a Overrides,
|
||||
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, overrides)?;
|
||||
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: BuiltinDirective::Bug.to_string(),
|
||||
title: "Error rendering admonishment".to_owned(),
|
||||
css_id: CssId::Prefix("admonition-".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);
|
||||
}
|
||||
}
|
||||
}
|
||||
269
src/preprocessor.rs
Normal file
269
src/preprocessor.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
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::{Overrides, 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 custom_directives = config
|
||||
.directive
|
||||
.custom
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
let on_failure = config.on_failure;
|
||||
let admonition_defaults = config.default;
|
||||
|
||||
let overrides = Overrides {
|
||||
book: admonition_defaults,
|
||||
custom: custom_directives,
|
||||
builtin: config.directive.builtin,
|
||||
};
|
||||
|
||||
// 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, &overrides, 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" role="note" aria-labelledby="admonition-title-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-title-title">
|
||||
|
||||
Title
|
||||
|
||||
</div>
|
||||
<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)
|
||||
}
|
||||
}
|
||||
134
src/render.rs
Normal file
134
src/render.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use mdbook::utils::unique_id_from_content;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{resolve::AdmonitionMeta, types::CssId};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Admonition<'a> {
|
||||
pub(crate) directive: String,
|
||||
pub(crate) title: String,
|
||||
pub(crate) content: Cow<'a, str>,
|
||||
pub(crate) css_id: CssId,
|
||||
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,
|
||||
css_id,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
} = info;
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
content: Cow::Borrowed(content),
|
||||
css_id,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
indent,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn html(self, id_counter: &mut HashMap<String, usize>) -> String {
|
||||
let anchor_id = match &self.css_id {
|
||||
CssId::Verbatim(id) => Cow::Borrowed(id.as_str()),
|
||||
CssId::Prefix(prefix) => {
|
||||
let id = unique_id_from_content(
|
||||
if !self.title.is_empty() {
|
||||
&self.title
|
||||
} else {
|
||||
ANCHOR_ID_DEFAULT
|
||||
},
|
||||
id_counter,
|
||||
);
|
||||
|
||||
Cow::Owned(format!("{}{}", prefix, id))
|
||||
}
|
||||
};
|
||||
|
||||
let title = &self.title;
|
||||
let content = &self.content;
|
||||
let indent = " ".repeat(self.indent);
|
||||
|
||||
let (titlebar_html, title_id) = if !title.is_empty() {
|
||||
let titlebar_element = if self.collapsible { "summary" } else { "div" };
|
||||
let title_id = format!("{anchor_id}-title");
|
||||
let titlebar_html = Cow::Owned(format!(
|
||||
r##"{indent}<{titlebar_element} class="admonition-title">
|
||||
{indent}<div id="{title_id}">
|
||||
{indent}
|
||||
{indent}{title}
|
||||
{indent}
|
||||
{indent}</div>
|
||||
{indent}<a class="admonition-anchor-link" href="#{anchor_id}"></a>
|
||||
{indent}</{titlebar_element}>
|
||||
"##
|
||||
));
|
||||
(titlebar_html, Some(title_id))
|
||||
} else {
|
||||
(Cow::Borrowed(""), None)
|
||||
};
|
||||
|
||||
let mut classes = vec![
|
||||
"admonition".to_owned(),
|
||||
format!("admonish-{}", self.directive),
|
||||
];
|
||||
classes.extend(self.additional_classnames);
|
||||
let classes = classes.join(" ");
|
||||
|
||||
let mut attributes = vec![
|
||||
("id", anchor_id),
|
||||
("class", Cow::Owned(classes)),
|
||||
("role", Cow::Borrowed("note")),
|
||||
];
|
||||
if let Some(title_id) = title_id {
|
||||
attributes.push(("aria-labelledby", Cow::Owned(title_id)));
|
||||
}
|
||||
let attributes = join_attributes(&attributes);
|
||||
|
||||
let admonition_element = 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.
|
||||
// - We should not indent the inner content, as it retains the indent
|
||||
// it is written with.
|
||||
format!(
|
||||
r#"
|
||||
{indent}<{admonition_element} {attributes}>
|
||||
{titlebar_html}{indent}<div>
|
||||
|
||||
{content}
|
||||
|
||||
{indent}</div>
|
||||
{indent}</{admonition_element}>"#,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
fn join_attributes(attributes: &[(impl AsRef<str>, impl AsRef<str>)]) -> String {
|
||||
let mut buffer = String::new();
|
||||
for (key, value) in attributes {
|
||||
buffer.push_str(key.as_ref());
|
||||
buffer.push_str(r#"=""#);
|
||||
buffer.push_str(value.as_ref());
|
||||
buffer.push_str(r#"" "#);
|
||||
}
|
||||
buffer.pop();
|
||||
buffer
|
||||
}
|
||||
|
||||
const ANCHOR_ID_DEFAULT: &str = "default";
|
||||
460
src/resolve.rs
Normal file
460
src/resolve.rs
Normal file
@@ -0,0 +1,460 @@
|
||||
use crate::config::InstanceConfig;
|
||||
use crate::types::{BuiltinDirective, CssId, CustomDirective, CustomDirectiveMap, Overrides};
|
||||
use std::fmt;
|
||||
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: String,
|
||||
pub title: String,
|
||||
pub css_id: CssId,
|
||||
pub additional_classnames: Vec<String>,
|
||||
pub collapsible: bool,
|
||||
}
|
||||
|
||||
/// Wrapper type to hold any value directive configuration.
|
||||
enum Directive {
|
||||
Builtin(BuiltinDirective),
|
||||
Custom(CustomDirective),
|
||||
}
|
||||
|
||||
impl fmt::Display for Directive {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Builtin(builtin) => builtin.fmt(f),
|
||||
Self::Custom(custom) => f.write_str(&custom.directive),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Directive {
|
||||
fn from_str(custom_directive_map: &CustomDirectiveMap, string: &str) -> Result<Self, ()> {
|
||||
if let Ok(builtin) = BuiltinDirective::from_str(string) {
|
||||
return Ok(Self::Builtin(builtin));
|
||||
}
|
||||
|
||||
if let Some(config) = custom_directive_map.get(string) {
|
||||
return Ok(Self::Custom(config.clone()));
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
|
||||
fn title(self, raw_directive: &str) -> String {
|
||||
match self {
|
||||
Directive::Builtin(_) => format_builtin_directive_title(raw_directive),
|
||||
Directive::Custom(custom) => custom
|
||||
.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| uppercase_first(raw_directive)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AdmonitionMeta {
|
||||
pub fn from_info_string(
|
||||
info_string: &str,
|
||||
overrides: &Overrides,
|
||||
) -> Option<Result<Self, String>> {
|
||||
InstanceConfig::from_info_string(info_string)
|
||||
.map(|raw| raw.map(|raw| Self::resolve(raw, overrides)))
|
||||
}
|
||||
|
||||
/// Combine the per-admonition configuration with global defaults (and
|
||||
/// other logic) to resolve the values needed for rendering.
|
||||
fn resolve(raw: InstanceConfig, overrides: &Overrides) -> Self {
|
||||
let InstanceConfig {
|
||||
directive: raw_directive,
|
||||
title,
|
||||
id,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
} = raw;
|
||||
|
||||
// Use values from block, else load default value
|
||||
let title = title.or_else(|| overrides.book.title.clone());
|
||||
|
||||
let directive = Directive::from_str(&overrides.custom, &raw_directive);
|
||||
|
||||
let collapsible = match directive {
|
||||
// If the directive is a builin one, use collapsible from block, else use default
|
||||
// value of the builtin directive, else use global default value
|
||||
Ok(Directive::Builtin(directive)) => collapsible.unwrap_or(
|
||||
overrides
|
||||
.builtin
|
||||
.get(&directive)
|
||||
.and_then(|config| config.collapsible)
|
||||
.unwrap_or(overrides.book.collapsible),
|
||||
),
|
||||
// If the directive is a custom one, use collapsible from block, else use default
|
||||
// value of the custom directive, else use global default value
|
||||
Ok(Directive::Custom(ref custom_dir)) => {
|
||||
collapsible.unwrap_or(custom_dir.collapsible.unwrap_or(overrides.book.collapsible))
|
||||
}
|
||||
Err(_) => collapsible.unwrap_or(overrides.book.collapsible),
|
||||
};
|
||||
|
||||
// Load the directive (and title, if one still not given)
|
||||
let (directive, title) = match (directive, title) {
|
||||
(Ok(directive), None) => (directive.to_string(), directive.title(&raw_directive)),
|
||||
(Err(_), None) => (BuiltinDirective::Note.to_string(), "Note".to_owned()),
|
||||
(Ok(directive), Some(title)) => (directive.to_string(), title),
|
||||
(Err(_), Some(title)) => (BuiltinDirective::Note.to_string(), title),
|
||||
};
|
||||
|
||||
let css_id = if let Some(verbatim) = id {
|
||||
CssId::Verbatim(verbatim)
|
||||
} else {
|
||||
const DEFAULT_CSS_ID_PREFIX: &str = "admonition-";
|
||||
CssId::Prefix(
|
||||
overrides
|
||||
.book
|
||||
.css_id_prefix
|
||||
.clone()
|
||||
.unwrap_or_else(|| DEFAULT_CSS_ID_PREFIX.to_owned()),
|
||||
)
|
||||
};
|
||||
|
||||
Self {
|
||||
directive,
|
||||
title,
|
||||
css_id,
|
||||
additional_classnames,
|
||||
collapsible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the title of an admonition directive
|
||||
///
|
||||
/// We special case a few words to make them look nicer (e.g. "tldr" -> "TL;DR" and "faq" -> "FAQ").
|
||||
fn format_builtin_directive_title(input: &str) -> String {
|
||||
match input {
|
||||
"tldr" => "TL;DR".to_owned(),
|
||||
"faq" => "FAQ".to_owned(),
|
||||
_ => uppercase_first(input),
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the first letter of `input` uppercase.
|
||||
///
|
||||
/// source: https://stackoverflow.com/a/38406885
|
||||
fn uppercase_first(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 std::collections::HashMap;
|
||||
|
||||
use crate::types::{AdmonitionDefaults, BuiltinDirectiveConfig};
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_format_builtin_directive_title() {
|
||||
assert_eq!(format_builtin_directive_title(""), "");
|
||||
assert_eq!(format_builtin_directive_title("a"), "A");
|
||||
assert_eq!(format_builtin_directive_title("tldr"), "TL;DR");
|
||||
assert_eq!(format_builtin_directive_title("faq"), "FAQ");
|
||||
assert_eq!(format_builtin_directive_title("note"), "Note");
|
||||
assert_eq!(format_builtin_directive_title("abstract"), "Abstract");
|
||||
// Unicode
|
||||
assert_eq!(format_builtin_directive_title("🦀"), "🦀");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: " ".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Overrides::default(),
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: "note".to_owned(),
|
||||
title: "Note".to_owned(),
|
||||
css_id: CssId::Prefix("admonition-".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,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Overrides {
|
||||
book: AdmonitionDefaults {
|
||||
title: Some("Important!!!".to_owned()),
|
||||
css_id_prefix: Some("custom-prefix-".to_owned()),
|
||||
collapsible: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: "note".to_owned(),
|
||||
title: "Important!!!".to_owned(),
|
||||
css_id: CssId::Prefix("custom-prefix-".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw_with_defaults_and_custom_id() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: " ".to_owned(),
|
||||
title: None,
|
||||
id: Some("my-custom-id".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Overrides {
|
||||
book: AdmonitionDefaults {
|
||||
title: Some("Important!!!".to_owned()),
|
||||
css_id_prefix: Some("ignored-custom-prefix-".to_owned()),
|
||||
collapsible: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: "note".to_owned(),
|
||||
title: "Important!!!".to_owned(),
|
||||
css_id: CssId::Verbatim("my-custom-id".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw_with_custom_directive() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: "frog".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Overrides {
|
||||
custom: [CustomDirective {
|
||||
directive: "frog".to_owned(),
|
||||
aliases: Vec::new(),
|
||||
title: None,
|
||||
collapsible: None,
|
||||
}]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: "frog".to_owned(),
|
||||
title: "Frog".to_owned(),
|
||||
css_id: CssId::Prefix("admonition-".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw_with_custom_directive_and_title() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: "frog".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Overrides {
|
||||
custom: [CustomDirective {
|
||||
directive: "frog".to_owned(),
|
||||
aliases: Vec::new(),
|
||||
title: Some("🏳️🌈".to_owned()),
|
||||
collapsible: None,
|
||||
}]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: "frog".to_owned(),
|
||||
title: "🏳️🌈".to_owned(),
|
||||
css_id: CssId::Prefix("admonition-".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw_with_custom_directive_alias() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: "toad".to_owned(),
|
||||
title: Some("Still a frog".to_owned()),
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Overrides {
|
||||
custom: [CustomDirective {
|
||||
directive: "frog".to_owned(),
|
||||
aliases: vec!["newt".to_owned(), "toad".to_owned()],
|
||||
title: Some("🏳️🌈".to_owned()),
|
||||
collapsible: None,
|
||||
}]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: "frog".to_owned(),
|
||||
title: "Still a frog".to_owned(),
|
||||
css_id: CssId::Prefix("admonition-".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw_with_collapsible_custom_directive() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: "frog".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Overrides {
|
||||
custom: [CustomDirective {
|
||||
directive: "frog".to_owned(),
|
||||
aliases: Vec::new(),
|
||||
title: None,
|
||||
collapsible: Some(true),
|
||||
}]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: "frog".to_owned(),
|
||||
title: "Frog".to_owned(),
|
||||
css_id: CssId::Prefix("admonition-".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw_with_collapsible_builtin_directive() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: "abstract".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Overrides {
|
||||
book: AdmonitionDefaults {
|
||||
title: None,
|
||||
css_id_prefix: None,
|
||||
collapsible: false,
|
||||
},
|
||||
builtin: HashMap::from([(
|
||||
BuiltinDirective::Abstract,
|
||||
BuiltinDirectiveConfig {
|
||||
collapsible: Some(true),
|
||||
}
|
||||
)]),
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: "abstract".to_owned(),
|
||||
title: "Abstract".to_owned(),
|
||||
css_id: CssId::Prefix("admonition-".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_admonition_info_from_raw_with_non_collapsible_builtin_directive() {
|
||||
assert_eq!(
|
||||
AdmonitionMeta::resolve(
|
||||
InstanceConfig {
|
||||
directive: "abstract".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Overrides {
|
||||
book: AdmonitionDefaults {
|
||||
title: None,
|
||||
css_id_prefix: None,
|
||||
collapsible: true,
|
||||
},
|
||||
builtin: HashMap::from([(
|
||||
BuiltinDirective::Abstract,
|
||||
BuiltinDirectiveConfig {
|
||||
collapsible: Some(false),
|
||||
}
|
||||
)]),
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: "abstract".to_owned(),
|
||||
title: "Abstract".to_owned(),
|
||||
css_id: CssId::Prefix("admonition-".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
20
src/test_data/mdbook-admonish-custom-expected.css
Normal file
20
src/test_data/mdbook-admonish-custom-expected.css
Normal file
@@ -0,0 +1,20 @@
|
||||
:root {
|
||||
--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>");
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-note) {
|
||||
border-color: #448aff;
|
||||
}
|
||||
|
||||
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
}
|
||||
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #448aff;
|
||||
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;
|
||||
}
|
||||
178
src/types.rs
Normal file
178
src/types.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
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,
|
||||
|
||||
#[serde(default)]
|
||||
// For backwards compatibility, we support this field with kebab-case style
|
||||
// naming, even though this was introduced in error.
|
||||
#[serde(alias = "css-id-prefix")]
|
||||
pub(crate) css_id_prefix: Option<String>,
|
||||
}
|
||||
|
||||
/// First class supported directives by the crate.
|
||||
///
|
||||
/// These are guaranteed to have valid CSS/icons available.
|
||||
///
|
||||
/// Custom directives can also be added via the book.toml config.
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Eq, Deserialize, Serialize, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum BuiltinDirective {
|
||||
Note,
|
||||
Abstract,
|
||||
Info,
|
||||
Tip,
|
||||
Success,
|
||||
Question,
|
||||
Warning,
|
||||
Failure,
|
||||
Danger,
|
||||
Bug,
|
||||
Example,
|
||||
Quote,
|
||||
}
|
||||
|
||||
impl FromStr for BuiltinDirective {
|
||||
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 fmt::Display for BuiltinDirective {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let value = match self {
|
||||
Self::Note => "note",
|
||||
Self::Abstract => "abstract",
|
||||
Self::Info => "info",
|
||||
Self::Tip => "tip",
|
||||
Self::Success => "success",
|
||||
Self::Question => "question",
|
||||
Self::Warning => "warning",
|
||||
Self::Failure => "failure",
|
||||
Self::Danger => "danger",
|
||||
Self::Bug => "bug",
|
||||
Self::Example => "example",
|
||||
Self::Quote => "quote",
|
||||
};
|
||||
f.write_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// The subset of information we care about during plugin runtime for custom directives.
|
||||
///
|
||||
/// This drops information only needed during CSS generation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct CustomDirective {
|
||||
pub directive: String,
|
||||
pub aliases: Vec<String>,
|
||||
pub title: Option<String>,
|
||||
pub collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<(String, crate::book_config::CustomDirective)> for CustomDirective {
|
||||
fn from((directive, config): (String, crate::book_config::CustomDirective)) -> Self {
|
||||
let crate::book_config::CustomDirective {
|
||||
aliases,
|
||||
title,
|
||||
collapsible,
|
||||
..
|
||||
} = config;
|
||||
Self {
|
||||
directive,
|
||||
aliases,
|
||||
title,
|
||||
collapsible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A map from the user given directive to underlying config.
|
||||
///
|
||||
/// The terminology is a bit mixed here - this map allows any input-directive,
|
||||
/// and returns the output-directive config.
|
||||
///
|
||||
/// i.e. this is the step alias mapping happens at
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct CustomDirectiveMap {
|
||||
inner: HashMap<String, CustomDirective>,
|
||||
}
|
||||
|
||||
impl CustomDirectiveMap {
|
||||
pub fn get(&self, key: &str) -> Option<&CustomDirective> {
|
||||
self.inner.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<CustomDirective> for CustomDirectiveMap {
|
||||
fn from_iter<I: IntoIterator<Item = CustomDirective>>(iter: I) -> Self {
|
||||
let mut inner = HashMap::default();
|
||||
for config in iter.into_iter() {
|
||||
inner
|
||||
.entry(config.directive.clone())
|
||||
.or_insert(config.clone());
|
||||
|
||||
for alias in config.aliases.iter() {
|
||||
inner.entry(alias.clone()).or_insert(config.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub(crate) struct BuiltinDirectiveConfig {
|
||||
/// Default collapsible value.
|
||||
#[serde(default)]
|
||||
pub collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum RenderTextMode {
|
||||
Strip,
|
||||
Html,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum CssId {
|
||||
/// id="my-id" in the admonishment
|
||||
///
|
||||
/// used directly for the id field
|
||||
Verbatim(String),
|
||||
/// the prefix from default.css_id_prefix (or "admonish-" if not specified)
|
||||
///
|
||||
/// will generate the rest of the id based on the title
|
||||
Prefix(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct Overrides {
|
||||
pub book: AdmonitionDefaults,
|
||||
pub builtin: HashMap<BuiltinDirective, BuiltinDirectiveConfig>,
|
||||
pub custom: CustomDirectiveMap,
|
||||
}
|
||||
13
v2.md
Normal file
13
v2.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# v2 Notes
|
||||
|
||||
## Compatibility to drop
|
||||
|
||||
- v1 config loading system from block info strings
|
||||
- v2 config loading system from block info strings
|
||||
- Support for `custom` configuration, moved to `directive.custom`
|
||||
- `css-id-prefix` kebab case
|
||||
|
||||
## Wishlist
|
||||
|
||||
- `mdbook` not to use a broken version of `toml` in the public api
|
||||
- `mdbook` to support loading css files from plugins
|
||||
Reference in New Issue
Block a user