mirror of
https://github.com/tommilligan/mdbook-admonish.git
synced 2025-12-27 13:30:47 -05:00
Compare commits
62 Commits
prep-1.15.
...
main
| 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 |
36
.github/workflows/check.yml
vendored
36
.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@v4
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v3
|
||||
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,34 +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@v4
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v3
|
||||
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@v3
|
||||
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
|
||||
@@ -71,16 +72,16 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-20.04
|
||||
- ubuntu-24.04
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- 1.66.0
|
||||
- 1.82.0
|
||||
experimental:
|
||||
- false
|
||||
include:
|
||||
# Run a canary test on nightly that's allowed to fail
|
||||
- os: ubuntu-20.04
|
||||
- os: ubuntu-24.04
|
||||
rust: nightly
|
||||
experimental: true
|
||||
# Test only stable on Windows, presume we'd get same result on other
|
||||
@@ -90,7 +91,7 @@ jobs:
|
||||
experimental: false
|
||||
exclude:
|
||||
# Don't bother retesting stable linux, we did it in the comprehensive test
|
||||
- os: ubuntu-20.04
|
||||
- os: ubuntu-24.04
|
||||
rust: stable
|
||||
experimental: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -105,7 +106,7 @@ jobs:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
@@ -113,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
|
||||
|
||||
30
.github/workflows/deploy.yml
vendored
30
.github/workflows/deploy.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
# binaries, so we use the same glibc version
|
||||
#
|
||||
# ref: https://github.com/rust-lang/mdBook/pull/1955
|
||||
os: ubuntu-20.04
|
||||
os: ubuntu-24.04
|
||||
name: x86_64-unknown-linux-gnu.tar.gz
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
@@ -43,19 +43,19 @@ jobs:
|
||||
|
||||
# Cache files between builds
|
||||
- name: Setup | Cache Cargo
|
||||
uses: actions/cache@v3
|
||||
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 | cross
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
|
||||
- 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]
|
||||
@@ -92,10 +92,12 @@ jobs:
|
||||
tar czvf ../../stage/${{ env.CRATE_NAME }}-${{ steps.extract_tag.outputs.tag }}-${{ matrix.name }} ${{ env.CRATE_NAME }}
|
||||
cd -
|
||||
- name: Post Setup | Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
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:
|
||||
@@ -109,18 +111,18 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup | Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Setup | Extract version
|
||||
shell: bash
|
||||
run: echo "##[set-output name=version;]$(echo ${GITHUB_REF#refs/tags/v})"
|
||||
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
id: extract_version
|
||||
|
||||
- name: Setup | Release notes
|
||||
run: |
|
||||
cat CHANGELOG.md | sed -n '/^## ${{ steps.extract_version.outputs.version }}$/,/^## /p' | sed '$d' > RELEASE.md
|
||||
- name: Build | Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ${{ env.CRATE_NAME }}-*/${{ env.CRATE_NAME }}-*
|
||||
body_path: RELEASE.md
|
||||
@@ -131,24 +133,22 @@ jobs:
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
needs: github_release
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
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
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Publish crate
|
||||
env:
|
||||
CARGO_LOGIN_TOKEN: ${{ secrets.CARGO_LOGIN_TOKEN }}
|
||||
|
||||
15
.github/workflows/docs.yml
vendored
15
.github/workflows/docs.yml
vendored
@@ -11,32 +11,35 @@ permissions:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
~/.cargo/bin
|
||||
cargo_target
|
||||
/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: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- 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.4.3
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: book/book
|
||||
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -1,6 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 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
|
||||
|
||||
|
||||
1726
Cargo.lock
generated
1726
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
33
Cargo.toml
33
Cargo.toml
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "mdbook-admonish"
|
||||
version = "1.15.0"
|
||||
version = "1.20.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.66.0"
|
||||
rust-version = "1.76.0"
|
||||
|
||||
authors = ["Tom Milligan <code@tommilligan.net>"]
|
||||
description = "A preprocessor for mdbook to add Material Design admonishments."
|
||||
@@ -26,26 +26,23 @@ name = "mdbook_admonish"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
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.3", default-features = false, features = ["std", "derive"], optional = true }
|
||||
env_logger = { version = "0.10", default-features = false, optional = true }
|
||||
log = "0.4.20"
|
||||
mdbook = "0.4.35"
|
||||
once_cell = "1.18.0"
|
||||
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.9.3"
|
||||
regex = "1.9.6"
|
||||
semver = "1.0.19"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.107"
|
||||
# The version of toml that mdbook uses internally (and uses in it's public api)
|
||||
# Only used for compatilibilty with the mdbook public api
|
||||
toml_mdbook = { package = "toml", version = "0.5.11" }
|
||||
toml = "0.8.1"
|
||||
toml_edit = { version = "0.20.1", optional = true }
|
||||
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]
|
||||
|
||||
@@ -21,9 +21,11 @@ into this:
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -4,17 +4,16 @@ 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 = "3.0.1" # do not edit: managed by `mdbook-admonish install`
|
||||
assets_version = "3.1.0" # do not edit: managed by `mdbook-admonish install`
|
||||
|
||||
[[preprocessor.admonish.custom]]
|
||||
directive = "expensive"
|
||||
icon = "./money-bag.svg"
|
||||
color = "#24ab38"
|
||||
[preprocessor.admonish.directive.custom]
|
||||
expensive = { icon = "./money-bag.svg", color = "#24ab38" }
|
||||
|
||||
[preprocessor.toc]
|
||||
command = "mdbook-toc"
|
||||
|
||||
@@ -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.
|
||||
```
|
||||
@@ -78,14 +78,19 @@ You can also configure the build to fail loudly, by setting `on_failure = "bail"
|
||||
|
||||
### Additional Options
|
||||
|
||||
You can pass additional options to each block. The options are structured as TOML key-value pairs.
|
||||
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 TOML string.
|
||||
Note that TOML escapes must be escaped again - for instance, write `\"` as `\\"`.
|
||||
A custom title can be provided:
|
||||
|
||||
````
|
||||
```admonish warning title="Data loss"
|
||||
@@ -114,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 title="_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 title="_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>, `*`.
|
||||
```
|
||||
@@ -148,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 class="custom-0 custom-1"
|
||||
```admonish note title="Stylish", class="custom-0 custom-1"
|
||||
Styled with my custom CSS class.
|
||||
```
|
||||
````
|
||||
@@ -173,7 +178,7 @@ 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"
|
||||
```admonish info title="My Info", id="my-special-info"
|
||||
Link to this block with `#my-special-info` instead of the default `#admonition-my-info`.
|
||||
```
|
||||
````
|
||||
@@ -183,14 +188,14 @@ Link to this block with `#my-special-info` instead of the default `#admonition-m
|
||||
For a block to be initially collapsible, and then be openable, set `collapsible=true`:
|
||||
|
||||
````
|
||||
```admonish 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 collapsible=true
|
||||
```admonish title="Sneaky", collapsible=true
|
||||
Content will be hidden initially.
|
||||
```
|
||||
|
||||
|
||||
@@ -75,28 +75,50 @@ Subfields:
|
||||
- For the `html` renderer, the default value is `html`.
|
||||
- For all other renderers, the default value is `preserve`.
|
||||
|
||||
### `custom`
|
||||
### `directive`
|
||||
|
||||
Optional.
|
||||
|
||||
Additional type of block to support.
|
||||
You must run `mdbook-admonish generate-custom` after updating these values, to generate the correct styles.
|
||||
Settings relating to each type of block.
|
||||
|
||||
Add blocks using TOML's [Array of Tables](https://toml.io/en/v1.0.0#array-of-tables) notation:
|
||||
#### `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.custom]]
|
||||
directive = "expensive"
|
||||
[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:
|
||||
|
||||
- `directive`: The keyword to use this type of block.
|
||||
- `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.
|
||||
|
||||
|
||||
2
compile_assets/.gitignore
vendored
2
compile_assets/.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
/node_modules
|
||||
*.css
|
||||
*.css.map
|
||||
.yarn/
|
||||
.pnp*
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6"
|
||||
}
|
||||
|
||||
@@ -92,8 +92,9 @@ $admonitions: (
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -200,6 +201,14 @@ a.admonition-anchor-link {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -222,6 +231,12 @@ summary.admonition-title {
|
||||
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 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ title = "mdbook-admonish-integration"
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "3.0.2" # 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]]
|
||||
|
||||
@@ -9,7 +9,7 @@ title = "mdbook-admonish-integration"
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "3.0.2" # 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]]
|
||||
|
||||
@@ -1,90 +1,108 @@
|
||||
<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>
|
||||
<div id="admonition-what-is-this" class="admonition admonish-abstract">
|
||||
<div id="admonition-what-is-this" class="admonition admonish-abstract" role="note" aria-labelledby="admonition-what-is-this-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-what-is-this-title">
|
||||
<p>What <i>is</i> this?</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-what-is-this"></a></p>
|
||||
</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 admonish-note">
|
||||
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-note-title">
|
||||
<p>Note</p>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note"></a></p>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>Simples</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="admonition-frog" class="admonition admonish-frog">
|
||||
<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>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-frog"></a></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">
|
||||
<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">
|
||||
<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>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a></p>
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a>
|
||||
</div>
|
||||
<div>
|
||||
<p>Failed with:</p>
|
||||
<pre><code class="language-log">TOML parsing error: TOML parse error at line 1, column 8
|
||||
<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 | title="
|
||||
| ^
|
||||
1 | config = { title=" }
|
||||
| ^
|
||||
invalid basic string
|
||||
|
||||
</code></pre>
|
||||
<p>Original markdown input:</p>
|
||||
<pre><code class="language-markdown">```admonish title="
|
||||
<pre><code class="language-markdown">```admonish title="
|
||||
No title, only body
|
||||
```
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<details id="admonition-note-1" class="admonition admonish-note">
|
||||
<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>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note-1"></a></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">
|
||||
<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>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-warning"></a></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">
|
||||
<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>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note-2"></a></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">
|
||||
<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>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note-3"></a></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)]
|
||||
@@ -108,15 +126,34 @@ let x = 20;
|
||||
</li>
|
||||
<li>
|
||||
<p>Thing two</p>
|
||||
<div id="admonition-note-4" class="admonition admonish-note">
|
||||
<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>
|
||||
<p><a class="admonition-anchor-link" href="#admonition-note-4"></a></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>
|
||||
|
||||
@@ -60,6 +60,13 @@ In a list:
|
||||
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
|
||||
|
||||
@@ -8,7 +8,7 @@ function eprintln() {
|
||||
>&2 echo "$1"
|
||||
}
|
||||
|
||||
VERSION="0.4.35"
|
||||
VERSION="0.4.51"
|
||||
|
||||
eprintln "Checking if mdbook $VERSION is installed"
|
||||
if [[ "$(mdbook --version)" != "mdbook v$VERSION" ]]; then
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.0.2
|
||||
3.1.0
|
||||
|
||||
@@ -86,6 +86,11 @@ html :is(.admonition-title, summary.admonition-title):last-child {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
@media print {
|
||||
details.admonition::details-content {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
details.admonition > summary.admonition-title::after {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
@@ -106,6 +111,9 @@ details.admonition > summary.admonition-title::after {
|
||||
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>");
|
||||
|
||||
@@ -125,7 +125,7 @@ struct Preprocessors {
|
||||
|
||||
/// Load the plugin specific config as a toml string, for private deserialization.
|
||||
fn admonish_config_string(config: &Config) -> Result<String> {
|
||||
Ok(toml_mdbook::to_string(
|
||||
Ok(toml::to_string(
|
||||
&config
|
||||
.preprocessor
|
||||
.admonish
|
||||
@@ -160,7 +160,7 @@ mod install {
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
};
|
||||
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",
|
||||
@@ -196,7 +196,7 @@ mod install {
|
||||
let toml = fs::read_to_string(&config)
|
||||
.with_context(|| format!("can't read configuration file '{}'", config.display()))?;
|
||||
let mut doc = toml
|
||||
.parse::<Document>()
|
||||
.parse::<DocumentMut>()
|
||||
.context("configuration is not valid TOML")?;
|
||||
|
||||
if let Ok(preprocessor) = preprocessor(&mut doc) {
|
||||
@@ -259,7 +259,7 @@ A beautifully styled message.
|
||||
/// 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());
|
||||
@@ -283,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());
|
||||
|
||||
@@ -4,26 +4,60 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::types::AdmonitionDefaults;
|
||||
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_mdbook::to_string(
|
||||
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> {
|
||||
toml::from_str(data).context("Invalid mdbook-admonish configuration in book.toml")
|
||||
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)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
/// 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,
|
||||
@@ -38,14 +72,50 @@ pub(crate) struct Config {
|
||||
pub assets_version: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub custom: Vec<CustomDirective>,
|
||||
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 {
|
||||
/// The primary directive. Used for CSS classnames
|
||||
pub directive: String,
|
||||
|
||||
/// Path to an SVG file, relative to the book root.
|
||||
pub icon: PathBuf,
|
||||
|
||||
@@ -59,6 +129,20 @@ pub(crate) struct CustomDirective {
|
||||
/// 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)]
|
||||
@@ -86,3 +170,156 @@ impl Default for OnFailure {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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)]
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub(crate) struct InstanceConfig {
|
||||
pub(crate) directive: String,
|
||||
pub(crate) title: Option<String>,
|
||||
@@ -35,20 +37,29 @@ impl InstanceConfig {
|
||||
/// - `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))
|
||||
}
|
||||
|
||||
// If we succeed at parsing v2, return that. Otherwise hold onto the error
|
||||
let config_v2_error = match v2::from_config_string(config_string) {
|
||||
Ok(config) => return Some(Ok(config)),
|
||||
Err(config) => config,
|
||||
/// 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,
|
||||
};
|
||||
|
||||
Some(if let Ok(config) = v1::from_config_string(config_string) {
|
||||
// If we succeed at parsing v1, return that.
|
||||
Ok(config)
|
||||
} else {
|
||||
// Otherwise return our v2 error.
|
||||
Err(config_v2_error)
|
||||
})
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,5 +101,20 @@ mod test {
|
||||
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}")
|
||||
}
|
||||
139
src/config/v2.rs
139
src/config/v2.rs
@@ -1,30 +1,16 @@
|
||||
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;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct UserInput {
|
||||
#[serde(default)]
|
||||
r#type: Option<String>,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
#[serde(default)]
|
||||
id: Option<String>,
|
||||
#[serde(default)]
|
||||
class: Option<String>,
|
||||
#[serde(default)]
|
||||
collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
/// Transform our config string into valid toml
|
||||
fn bare_key_value_pairs_to_toml(pairs: &str) -> String {
|
||||
use regex::Captures;
|
||||
|
||||
static RX_BARE_KEY_ASSIGNMENT: Lazy<Regex> = Lazy::new(|| {
|
||||
let bare_key = r#"[A-Za-z0-9_-]+"#;
|
||||
Regex::new(&format!("(?:{bare_key}) *=")).expect("bare key assignment regex")
|
||||
});
|
||||
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!(
|
||||
@@ -41,10 +27,18 @@ fn bare_key_value_pairs_to_toml(pairs: &str) -> String {
|
||||
.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();
|
||||
@@ -52,7 +46,7 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig,
|
||||
let config: UserInput = match toml::from_str(config_toml) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
let original_error = Err(format!("TOML parsing error: {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,
|
||||
@@ -62,17 +56,11 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig,
|
||||
None => (config_toml, ""),
|
||||
};
|
||||
|
||||
static RX_DIRECTIVE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^[A-Za-z0-9_-]+$"#).expect("directive regex"));
|
||||
|
||||
if !RX_DIRECTIVE.is_match(directive) {
|
||||
return original_error;
|
||||
return Err(format_invalid_directive(directive, original_error));
|
||||
}
|
||||
|
||||
let mut config: UserInput = match toml::from_str(config_toml) {
|
||||
Ok(config) => config,
|
||||
Err(_) => return original_error,
|
||||
};
|
||||
let mut config = user_input_from_config_toml(config_toml)?;
|
||||
config.r#type = Some(directive.to_owned());
|
||||
config
|
||||
}
|
||||
@@ -102,97 +90,120 @@ mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_v2() {
|
||||
assert_eq!(
|
||||
from_config_string("").unwrap(),
|
||||
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,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(" ").unwrap(),
|
||||
},
|
||||
)?;
|
||||
check(
|
||||
" ",
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(
|
||||
r#"type="note" class="additional classname" title="Никита" collapsible=true"#
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
)?;
|
||||
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
|
||||
assert_eq!(
|
||||
from_config_string(r#"unkonwn="but valid toml""#).unwrap(),
|
||||
check(
|
||||
r#"unkonwn="but valid toml""#,
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
},
|
||||
)?;
|
||||
// Just directive is fine
|
||||
assert_eq!(
|
||||
from_config_string(r#"info"#).unwrap(),
|
||||
check(
|
||||
r#"info"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
},
|
||||
)?;
|
||||
// Directive plus toml config
|
||||
assert_eq!(
|
||||
from_config_string(r#"info title="Information" collapsible=false"#).unwrap(),
|
||||
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
|
||||
assert_eq!(
|
||||
from_config_string(r#"info title="My Info" id="my-info-custom-id""#).unwrap(),
|
||||
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 6
|
||||
r#"TOML parsing error: TOML parse error at line 1, column 9
|
||||
|
|
||||
1 | note
|
||||
| ^
|
||||
expected `.`, `=`
|
||||
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
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ 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
|
||||
// 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"#;
|
||||
//
|
||||
@@ -70,7 +70,7 @@ fn directive_css(name: &str, svg_data: &str, tint: HexColor) -> String {
|
||||
#[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.custom;
|
||||
let custom_directives = config.directive.custom;
|
||||
|
||||
if custom_directives.is_empty() {
|
||||
return Err(anyhow!("No custom directives provided"));
|
||||
@@ -78,10 +78,10 @@ pub fn css_from_config(book_dir: &Path, config: &str) -> Result<String> {
|
||||
|
||||
log::info!("Loaded {} custom directives", custom_directives.len());
|
||||
let mut css = String::new();
|
||||
for directive in custom_directives.iter() {
|
||||
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.directive, &svg, directive.color));
|
||||
css.push_str(&directive_css(directive_name, &svg, directive.color));
|
||||
}
|
||||
Ok(css)
|
||||
}
|
||||
|
||||
318
src/markdown.rs
318
src/markdown.rs
@@ -4,14 +4,13 @@ use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag};
|
||||
use crate::{
|
||||
book_config::OnFailure,
|
||||
parse::parse_admonition,
|
||||
types::{AdmonitionDefaults, CustomDirectiveMap, RenderTextMode},
|
||||
types::{Overrides, RenderTextMode},
|
||||
};
|
||||
|
||||
pub(crate) fn preprocess(
|
||||
content: &str,
|
||||
on_failure: OnFailure,
|
||||
admonition_defaults: &AdmonitionDefaults,
|
||||
custom_directives: &CustomDirectiveMap,
|
||||
overrides: &Overrides,
|
||||
render_text_mode: RenderTextMode,
|
||||
) -> MdbookResult<String> {
|
||||
let mut id_counter = Default::default();
|
||||
@@ -33,8 +32,7 @@ pub(crate) fn preprocess(
|
||||
|
||||
let admonition = match parse_admonition(
|
||||
info_string.as_ref(),
|
||||
admonition_defaults,
|
||||
custom_directives,
|
||||
overrides,
|
||||
span_content,
|
||||
on_failure,
|
||||
indent,
|
||||
@@ -92,9 +90,12 @@ fn indent_of(content: &str, position: usize, max: usize) -> usize {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::types::AdmonitionDefaults;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn indent_of_samples() {
|
||||
for (content, position, max, expected) in [
|
||||
@@ -137,8 +138,7 @@ mod test {
|
||||
preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults::default(),
|
||||
&CustomDirectiveMap::default(),
|
||||
&Overrides::default(),
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap()
|
||||
@@ -155,11 +155,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-note" class="admonition admonish-note">
|
||||
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-note-title">
|
||||
|
||||
Note
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -187,11 +189,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-note" class="admonition admonish-note">
|
||||
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-note-title">
|
||||
|
||||
Note
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -219,11 +223,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-warning" class="admonition admonish-warning">
|
||||
<div id="admonition-warning" class="admonition admonish-warning" role="note" aria-labelledby="admonition-warning-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-warning-title">
|
||||
|
||||
Warning
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-warning"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -249,11 +255,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-caution" class="admonition admonish-warning">
|
||||
<div id="admonition-caution" class="admonition admonish-warning" role="note" aria-labelledby="admonition-caution-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-caution-title">
|
||||
|
||||
Caution
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-caution"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -279,11 +287,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-read-this" class="admonition admonish-warning">
|
||||
<div id="admonition-read-this" class="admonition admonish-warning" role="note" aria-labelledby="admonition-read-this-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-read-this-title">
|
||||
|
||||
Read **this**!
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-read-this"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -373,11 +383,13 @@ hello
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-and-in-the-title" class="admonition admonish-note">
|
||||
<div id="admonition-and-in-the-title" class="admonition admonish-note" role="note" aria-labelledby="admonition-and-in-the-title-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-and-in-the-title-title">
|
||||
|
||||
And "<i>in</i>" the title
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-and-in-the-title"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -403,11 +415,13 @@ hello
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-trademark" class="admonition admonish-warning">
|
||||
<div id="admonition-trademark" class="admonition admonish-warning" role="note" aria-labelledby="admonition-trademark-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-trademark-title">
|
||||
|
||||
Trademark™
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-trademark"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -432,11 +446,13 @@ Will have bonus classnames
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-tip" class="admonition admonish-tip my-style other-style">
|
||||
<div id="admonition-tip" class="admonition admonish-tip my-style other-style" role="note" aria-labelledby="admonition-tip-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-tip-title">
|
||||
|
||||
Tip
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-tip"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -460,11 +476,13 @@ Will have bonus classnames
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-developers-dont-want-you-to-know-this-one-weird-tip" class="admonition admonish-tip my-style other-style">
|
||||
<div id="admonition-developers-dont-want-you-to-know-this-one-weird-tip" class="admonition admonish-tip my-style other-style" role="note" aria-labelledby="admonition-developers-dont-want-you-to-know-this-one-weird-tip-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-developers-dont-want-you-to-know-this-one-weird-tip-title">
|
||||
|
||||
Developers don't want you to know this one weird tip!
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-developers-dont-want-you-to-know-this-one-weird-tip"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -487,7 +505,7 @@ Will have bonus classnames
|
||||
|
||||
let expected = r#"
|
||||
|
||||
<div id="admonition-default" class="admonition admonish-note">
|
||||
<div id="admonition-default" class="admonition admonish-note" role="note">
|
||||
<div>
|
||||
|
||||
|
||||
@@ -513,11 +531,13 @@ Content one.
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-my-note" class="admonition admonish-note">
|
||||
<div id="admonition-my-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-my-note-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-my-note-title">
|
||||
|
||||
My Note
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-my-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -528,11 +548,13 @@ Content zero.
|
||||
</div>
|
||||
|
||||
|
||||
<div id="admonition-my-note-1" class="admonition admonish-note">
|
||||
<div id="admonition-my-note-1" class="admonition admonish-note" role="note" aria-labelledby="admonition-my-note-1-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-my-note-1-title">
|
||||
|
||||
My Note
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-my-note-1"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -556,11 +578,13 @@ Bonus content!
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-article-heading" class="admonition admonish-tip my other-style">
|
||||
<div id="admonition-article-heading" class="admonition admonish-tip my other-style" role="note" aria-labelledby="admonition-article-heading-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-article-heading-title">
|
||||
|
||||
Article Heading
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-article-heading"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -584,11 +608,13 @@ Bonus content!
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<div id="admonition-error-rendering-admonishment" class="admonition admonish-bug">
|
||||
<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">
|
||||
|
||||
Error rendering admonishment
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -596,10 +622,12 @@ Error rendering admonishment
|
||||
Failed with:
|
||||
|
||||
```log
|
||||
TOML parsing error: TOML parse error at line 1, column 8
|
||||
'title="' is not a valid directive or TOML key-value pair.
|
||||
|
||||
TOML parsing error: TOML parse error at line 1, column 21
|
||||
|
|
||||
1 | title="
|
||||
| ^
|
||||
1 | config = { title=" }
|
||||
| ^
|
||||
invalid basic string
|
||||
|
||||
```
|
||||
@@ -631,8 +659,7 @@ Bonus content!
|
||||
preprocess(
|
||||
content,
|
||||
OnFailure::Bail,
|
||||
&AdmonitionDefaults::default(),
|
||||
&CustomDirectiveMap::default(),
|
||||
&Overrides::default(),
|
||||
RenderTextMode::Html
|
||||
)
|
||||
.unwrap_err()
|
||||
@@ -659,8 +686,7 @@ x = 20;
|
||||
preprocess(
|
||||
content,
|
||||
OnFailure::Bail,
|
||||
&AdmonitionDefaults::default(),
|
||||
&CustomDirectiveMap::default(),
|
||||
&Overrides::default(),
|
||||
RenderTextMode::Strip
|
||||
)
|
||||
.unwrap(),
|
||||
@@ -686,11 +712,13 @@ Hidden
|
||||
|
||||
let expected = r##"
|
||||
|
||||
<details id="admonition-note" class="admonition admonish-note">
|
||||
<details id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
|
||||
<summary class="admonition-title">
|
||||
<div id="admonition-note-title">
|
||||
|
||||
Note
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</summary>
|
||||
<div>
|
||||
@@ -715,11 +743,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-admonish" class="admonition admonish-note">
|
||||
<div id="admonition-admonish" class="admonition admonish-note" role="note" aria-labelledby="admonition-admonish-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-admonish-title">
|
||||
|
||||
Admonish
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-admonish"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -734,12 +764,14 @@ Text
|
||||
let preprocess_result = preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Admonish".to_owned()),
|
||||
css_id_prefix: None,
|
||||
collapsible: false,
|
||||
&Overrides {
|
||||
book: AdmonitionDefaults {
|
||||
title: Some("Admonish".to_owned()),
|
||||
css_id_prefix: None,
|
||||
collapsible: false,
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&CustomDirectiveMap::default(),
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -757,7 +789,7 @@ Text
|
||||
|
||||
let expected = r#"# Chapter
|
||||
|
||||
<div id="admonition-default" class="admonition admonish-note">
|
||||
<div id="admonition-default" class="admonition admonish-note" role="note">
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
@@ -770,12 +802,14 @@ Text
|
||||
let preprocess_result = preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Admonish".to_owned()),
|
||||
css_id_prefix: None,
|
||||
collapsible: false,
|
||||
&Overrides {
|
||||
book: AdmonitionDefaults {
|
||||
title: Some("Admonish".to_owned()),
|
||||
css_id_prefix: None,
|
||||
collapsible: false,
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&CustomDirectiveMap::default(),
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -793,7 +827,7 @@ Text
|
||||
|
||||
let expected = r#"# Chapter
|
||||
|
||||
<div id="admonition-default" class="admonition admonish-note">
|
||||
<div id="admonition-default" class="admonition admonish-note" role="note">
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
@@ -817,11 +851,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="yay-custom-id" class="admonition admonish-success">
|
||||
<div id="yay-custom-id" class="admonition admonish-success" role="note" aria-labelledby="yay-custom-id-title">
|
||||
<div class="admonition-title">
|
||||
<div id="yay-custom-id-title">
|
||||
|
||||
Check
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#yay-custom-id"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -847,11 +883,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-check" class="admonition admonish-success">
|
||||
<div id="admonition-check" class="admonition admonish-success" role="note" aria-labelledby="admonition-check-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-check-title">
|
||||
|
||||
Check
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-check"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -877,17 +915,54 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-check-mark" class="admonition admonish-success">
|
||||
<div id="admonition-check-mark" class="admonition admonish-success" role="note" aria-labelledby="admonition-check-mark-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-check-mark-title">
|
||||
|
||||
Check Mark
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-check-mark"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_and_content_with_html() {
|
||||
// Note that we use toml literal (single quoted) strings here
|
||||
// and the fact we have an equals sign in the value does not cause
|
||||
// us to break (because we're using v3 syntax, not v2)
|
||||
let content = r#"# Chapter
|
||||
```admonish success title='Check <span class="emphasis">Mark</span>'
|
||||
A <span class="emphasis">simple</span> admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-check-mark" class="admonition admonish-success" role="note" aria-labelledby="admonition-check-mark-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-check-mark-title">
|
||||
|
||||
Check <span class="emphasis">Mark</span>
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-check-mark"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A <span class="emphasis">simple</span> admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
@@ -907,11 +982,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="info" class="admonition admonish-info">
|
||||
<div id="info" class="admonition admonish-info" role="note" aria-labelledby="info-title">
|
||||
<div class="admonition-title">
|
||||
<div id="info-title">
|
||||
|
||||
Info
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#info"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -926,12 +1003,14 @@ Text
|
||||
let preprocess_result = preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Info".to_owned()),
|
||||
css_id_prefix: Some("".to_owned()),
|
||||
collapsible: false,
|
||||
&Overrides {
|
||||
book: AdmonitionDefaults {
|
||||
title: Some("Info".to_owned()),
|
||||
css_id_prefix: Some("".to_owned()),
|
||||
collapsible: false,
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&CustomDirectiveMap::default(),
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -949,11 +1028,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="prefix-my-title" class="admonition admonish-info">
|
||||
<div id="prefix-my-title" class="admonition admonish-info" role="note" aria-labelledby="prefix-my-title-title">
|
||||
<div class="admonition-title">
|
||||
<div id="prefix-my-title-title">
|
||||
|
||||
My Title
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#prefix-my-title"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -968,12 +1049,14 @@ Text
|
||||
let preprocess_result = preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Info".to_owned()),
|
||||
css_id_prefix: Some("prefix-".to_owned()),
|
||||
collapsible: false,
|
||||
&Overrides {
|
||||
book: AdmonitionDefaults {
|
||||
title: Some("Info".to_owned()),
|
||||
css_id_prefix: Some("prefix-".to_owned()),
|
||||
collapsible: false,
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&CustomDirectiveMap::default(),
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -991,11 +1074,13 @@ Text
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="my-section-id" class="admonition admonish-info">
|
||||
<div id="my-section-id" class="admonition admonish-info" role="note" aria-labelledby="my-section-id-title">
|
||||
<div class="admonition-title">
|
||||
<div id="my-section-id-title">
|
||||
|
||||
My Title
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#my-section-id"></a>
|
||||
</div>
|
||||
<div>
|
||||
@@ -1010,12 +1095,14 @@ Text
|
||||
let preprocess_result = preprocess(
|
||||
content,
|
||||
OnFailure::Continue,
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Info".to_owned()),
|
||||
css_id_prefix: Some("ignored-prefix-".to_owned()),
|
||||
collapsible: false,
|
||||
&Overrides {
|
||||
book: AdmonitionDefaults {
|
||||
title: Some("Info".to_owned()),
|
||||
css_id_prefix: Some("ignored-prefix-".to_owned()),
|
||||
collapsible: false,
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&CustomDirectiveMap::default(),
|
||||
RenderTextMode::Html,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1056,17 +1143,19 @@ Text
|
||||
1. Thing two
|
||||
|
||||
|
||||
<div id="admonition-note" class="admonition admonish-note">
|
||||
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-note-title">
|
||||
|
||||
Note
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Thing two
|
||||
|
||||
|
||||
Thing two
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1079,4 +1168,97 @@ Text
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/tommilligan/mdbook-admonish/issues/223
|
||||
#[test]
|
||||
fn nested_list_should_not_render_indented_code_block() {
|
||||
let content = r#"# Chapter
|
||||
|
||||
- Level one
|
||||
|
||||
```admonish
|
||||
Thing one
|
||||
line two
|
||||
```
|
||||
|
||||
- Level two
|
||||
|
||||
```admonish
|
||||
Thing two
|
||||
line two
|
||||
```
|
||||
|
||||
- Level three
|
||||
|
||||
```admonish
|
||||
Thing three
|
||||
line two
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
- Level one
|
||||
|
||||
|
||||
<div id="admonition-note" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-note-title">
|
||||
|
||||
Note
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Thing one
|
||||
line two
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
- Level two
|
||||
|
||||
|
||||
<div id="admonition-note-1" class="admonition admonish-note" role="note" aria-labelledby="admonition-note-1-title">
|
||||
<div class="admonition-title">
|
||||
<div id="admonition-note-1-title">
|
||||
|
||||
Note
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note-1"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Thing two
|
||||
line two
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
- Level three
|
||||
|
||||
|
||||
<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">
|
||||
|
||||
Note
|
||||
|
||||
</div>
|
||||
<a class="admonition-anchor-link" href="#admonition-note-2"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Thing three
|
||||
line two
|
||||
|
||||
</div>
|
||||
</div>
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
book_config::OnFailure,
|
||||
render::Admonition,
|
||||
resolve::AdmonitionMeta,
|
||||
types::{AdmonitionDefaults, BuiltinDirective, CssId, CustomDirectiveMap},
|
||||
types::{BuiltinDirective, CssId, Overrides},
|
||||
};
|
||||
|
||||
/// Given the content in the span of the code block, and the info string,
|
||||
@@ -19,8 +19,7 @@ use crate::{
|
||||
/// If the code block is not an admonition, return `None`.
|
||||
pub(crate) fn parse_admonition<'a>(
|
||||
info_string: &'a str,
|
||||
admonition_defaults: &'a AdmonitionDefaults,
|
||||
custom_directives: &'a CustomDirectiveMap,
|
||||
overrides: &'a Overrides,
|
||||
content: &'a str,
|
||||
on_failure: OnFailure,
|
||||
indent: usize,
|
||||
@@ -28,8 +27,7 @@ pub(crate) fn parse_admonition<'a>(
|
||||
// We need to know fence details anyway for error messages
|
||||
let extracted = extract_admonish_body(content);
|
||||
|
||||
let info =
|
||||
AdmonitionMeta::from_info_string(info_string, admonition_defaults, custom_directives)?;
|
||||
let info = AdmonitionMeta::from_info_string(info_string, overrides)?;
|
||||
let info = match info {
|
||||
Ok(info) => info,
|
||||
Err(message) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ use mdbook::{
|
||||
use crate::{
|
||||
book_config::{admonish_config_from_context, Config, RenderMode},
|
||||
markdown::preprocess,
|
||||
types::{CustomDirectiveMap, RenderTextMode},
|
||||
types::{Overrides, RenderTextMode},
|
||||
};
|
||||
|
||||
pub struct Admonish;
|
||||
@@ -22,11 +22,21 @@ impl Preprocessor for Admonish {
|
||||
let config = admonish_config_from_context(ctx)?;
|
||||
ensure_compatible_assets_version(&config)?;
|
||||
|
||||
let custom_directives =
|
||||
CustomDirectiveMap::from_configs(config.custom.into_iter().map(Into::into));
|
||||
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
|
||||
@@ -55,16 +65,11 @@ impl Preprocessor for Admonish {
|
||||
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
res = Some(
|
||||
preprocess(
|
||||
&chapter.content,
|
||||
on_failure,
|
||||
&admonition_defaults,
|
||||
&custom_directives,
|
||||
render_text_mode,
|
||||
)
|
||||
.map(|md| {
|
||||
chapter.content = md;
|
||||
}),
|
||||
preprocess(&chapter.content, on_failure, &overrides, render_text_mode).map(
|
||||
|md| {
|
||||
chapter.content = md;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -173,11 +178,13 @@ x = 20;
|
||||
"#;
|
||||
let expected_content = r##"
|
||||
|
||||
<div id="admonition-title" class="admonition admonish-note">
|
||||
<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>
|
||||
|
||||
@@ -35,9 +35,9 @@ impl<'a> Admonition<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn html(&self, id_counter: &mut HashMap<String, usize>) -> String {
|
||||
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),
|
||||
CssId::Verbatim(id) => Cow::Borrowed(id.as_str()),
|
||||
CssId::Prefix(prefix) => {
|
||||
let id = unique_id_from_content(
|
||||
if !self.title.is_empty() {
|
||||
@@ -56,53 +56,79 @@ impl<'a> Admonition<'a> {
|
||||
let content = &self.content;
|
||||
let indent = " ".repeat(self.indent);
|
||||
|
||||
let title_block = if self.collapsible { "summary" } else { "div" };
|
||||
|
||||
let title_html = if !title.is_empty() {
|
||||
Cow::Owned(format!(
|
||||
r##"{indent}<{title_block} class="admonition-title">
|
||||
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}</{title_block}>
|
||||
{indent}</{titlebar_element}>
|
||||
"##
|
||||
))
|
||||
));
|
||||
(titlebar_html, Some(title_id))
|
||||
} else {
|
||||
Cow::Borrowed("")
|
||||
(Cow::Borrowed(""), None)
|
||||
};
|
||||
|
||||
let mut additional_class = format!("admonish-{}", self.directive);
|
||||
if !self.additional_classnames.is_empty() {
|
||||
for additional_classname in &self.additional_classnames {
|
||||
additional_class.push(' ');
|
||||
additional_class.push_str(additional_classname);
|
||||
}
|
||||
}
|
||||
let mut classes = vec![
|
||||
"admonition".to_owned(),
|
||||
format!("admonish-{}", self.directive),
|
||||
];
|
||||
classes.extend(self.additional_classnames);
|
||||
let classes = classes.join(" ");
|
||||
|
||||
let admonition_block = if self.collapsible { "details" } else { "div" };
|
||||
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_block} id="{anchor_id}" class="admonition {additional_class}">
|
||||
{title_html}{indent}<div>
|
||||
{indent}
|
||||
{indent}{content}
|
||||
{indent}
|
||||
{indent}<{admonition_element} {attributes}>
|
||||
{titlebar_html}{indent}<div>
|
||||
|
||||
{content}
|
||||
|
||||
{indent}</div>
|
||||
{indent}</{admonition_block}>"#,
|
||||
{indent}</{admonition_element}>"#,
|
||||
)
|
||||
}
|
||||
|
||||
/// Strips all admonish syntax, leaving the plain content of the block.
|
||||
pub(crate) fn strip(&self) -> String {
|
||||
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";
|
||||
|
||||
232
src/resolve.rs
232
src/resolve.rs
@@ -1,7 +1,5 @@
|
||||
use crate::config::InstanceConfig;
|
||||
use crate::types::{
|
||||
AdmonitionDefaults, BuiltinDirective, CssId, CustomDirective, CustomDirectiveMap,
|
||||
};
|
||||
use crate::types::{BuiltinDirective, CssId, CustomDirective, CustomDirectiveMap, Overrides};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -59,20 +57,15 @@ impl Directive {
|
||||
impl AdmonitionMeta {
|
||||
pub fn from_info_string(
|
||||
info_string: &str,
|
||||
defaults: &AdmonitionDefaults,
|
||||
custom_directives: &CustomDirectiveMap,
|
||||
overrides: &Overrides,
|
||||
) -> Option<Result<Self, String>> {
|
||||
InstanceConfig::from_info_string(info_string)
|
||||
.map(|raw| raw.map(|raw| Self::resolve(raw, defaults, custom_directives)))
|
||||
.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,
|
||||
defaults: &AdmonitionDefaults,
|
||||
custom_directives: &CustomDirectiveMap,
|
||||
) -> Self {
|
||||
fn resolve(raw: InstanceConfig, overrides: &Overrides) -> Self {
|
||||
let InstanceConfig {
|
||||
directive: raw_directive,
|
||||
title,
|
||||
@@ -82,10 +75,27 @@ impl AdmonitionMeta {
|
||||
} = raw;
|
||||
|
||||
// Use values from block, else load default value
|
||||
let title = title.or_else(|| defaults.title.clone());
|
||||
let collapsible = collapsible.unwrap_or(defaults.collapsible);
|
||||
let title = title.or_else(|| overrides.book.title.clone());
|
||||
|
||||
let directive = Directive::from_str(custom_directives, &raw_directive);
|
||||
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) {
|
||||
@@ -100,7 +110,8 @@ impl AdmonitionMeta {
|
||||
} else {
|
||||
const DEFAULT_CSS_ID_PREFIX: &str = "admonition-";
|
||||
CssId::Prefix(
|
||||
defaults
|
||||
overrides
|
||||
.book
|
||||
.css_id_prefix
|
||||
.clone()
|
||||
.unwrap_or_else(|| DEFAULT_CSS_ID_PREFIX.to_owned()),
|
||||
@@ -141,6 +152,10 @@ fn uppercase_first(input: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::types::{AdmonitionDefaults, BuiltinDirectiveConfig};
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -167,8 +182,7 @@ mod test {
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&Default::default(),
|
||||
&CustomDirectiveMap::default(),
|
||||
&Overrides::default(),
|
||||
),
|
||||
AdmonitionMeta {
|
||||
directive: "note".to_owned(),
|
||||
@@ -191,12 +205,14 @@ mod test {
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Important!!!".to_owned()),
|
||||
css_id_prefix: Some("custom-prefix-".to_owned()),
|
||||
collapsible: true,
|
||||
},
|
||||
&CustomDirectiveMap::default(),
|
||||
&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(),
|
||||
@@ -219,12 +235,14 @@ mod test {
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&AdmonitionDefaults {
|
||||
title: Some("Important!!!".to_owned()),
|
||||
css_id_prefix: Some("ignored-custom-prefix-".to_owned()),
|
||||
collapsible: true,
|
||||
},
|
||||
&CustomDirectiveMap::default(),
|
||||
&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(),
|
||||
@@ -247,12 +265,17 @@ mod test {
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&AdmonitionDefaults::default(),
|
||||
&CustomDirectiveMap::from_configs(vec![CustomDirective {
|
||||
directive: "frog".to_owned(),
|
||||
aliases: Vec::new(),
|
||||
title: 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(),
|
||||
@@ -275,12 +298,17 @@ mod test {
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&AdmonitionDefaults::default(),
|
||||
&CustomDirectiveMap::from_configs(vec![CustomDirective {
|
||||
directive: "frog".to_owned(),
|
||||
aliases: Vec::new(),
|
||||
title: Some("🏳️🌈".to_owned()),
|
||||
}]),
|
||||
&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(),
|
||||
@@ -303,12 +331,17 @@ mod test {
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
&AdmonitionDefaults::default(),
|
||||
&CustomDirectiveMap::from_configs(vec![CustomDirective {
|
||||
directive: "frog".to_owned(),
|
||||
aliases: vec!["newt".to_owned(), "toad".to_owned()],
|
||||
title: Some("🏳️🌈".to_owned()),
|
||||
}]),
|
||||
&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(),
|
||||
@@ -319,4 +352,109 @@ mod test {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
52
src/types.rs
52
src/types.rs
@@ -5,7 +5,6 @@ use std::str::FromStr;
|
||||
|
||||
/// Book wide defaults that may be provided by the user.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub(crate) struct AdmonitionDefaults {
|
||||
#[serde(default)]
|
||||
pub(crate) title: Option<String>,
|
||||
@@ -14,6 +13,9 @@ pub(crate) struct AdmonitionDefaults {
|
||||
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>,
|
||||
}
|
||||
|
||||
@@ -22,7 +24,8 @@ pub(crate) struct AdmonitionDefaults {
|
||||
/// These are guaranteed to have valid CSS/icons available.
|
||||
///
|
||||
/// Custom directives can also be added via the book.toml config.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Eq, Deserialize, Serialize, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum BuiltinDirective {
|
||||
Note,
|
||||
Abstract,
|
||||
@@ -83,25 +86,27 @@ impl fmt::Display for BuiltinDirective {
|
||||
/// The subset of information we care about during plugin runtime for custom directives.
|
||||
///
|
||||
/// This drops information only needed during CSS generation.
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct CustomDirective {
|
||||
pub directive: String,
|
||||
pub aliases: Vec<String>,
|
||||
pub title: Option<String>,
|
||||
pub collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<crate::book_config::CustomDirective> for CustomDirective {
|
||||
fn from(other: crate::book_config::CustomDirective) -> Self {
|
||||
impl From<(String, crate::book_config::CustomDirective)> for CustomDirective {
|
||||
fn from((directive, config): (String, crate::book_config::CustomDirective)) -> Self {
|
||||
let crate::book_config::CustomDirective {
|
||||
directive,
|
||||
aliases,
|
||||
title,
|
||||
collapsible,
|
||||
..
|
||||
} = other;
|
||||
} = config;
|
||||
Self {
|
||||
directive,
|
||||
aliases,
|
||||
title,
|
||||
collapsible,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +117,7 @@ impl From<crate::book_config::CustomDirective> for CustomDirective {
|
||||
/// and returns the output-directive config.
|
||||
///
|
||||
/// i.e. this is the step alias mapping happens at
|
||||
#[derive(Default)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct CustomDirectiveMap {
|
||||
inner: HashMap<String, CustomDirective>,
|
||||
}
|
||||
@@ -121,19 +126,18 @@ impl CustomDirectiveMap {
|
||||
pub fn get(&self, key: &str) -> Option<&CustomDirective> {
|
||||
self.inner.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_configs<T>(configs: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = CustomDirective>,
|
||||
{
|
||||
impl FromIterator<CustomDirective> for CustomDirectiveMap {
|
||||
fn from_iter<I: IntoIterator<Item = CustomDirective>>(iter: I) -> Self {
|
||||
let mut inner = HashMap::default();
|
||||
for directive in configs.into_iter() {
|
||||
for config in iter.into_iter() {
|
||||
inner
|
||||
.entry(directive.directive.clone())
|
||||
.or_insert(directive.clone());
|
||||
.entry(config.directive.clone())
|
||||
.or_insert(config.clone());
|
||||
|
||||
for alias in directive.aliases.iter() {
|
||||
inner.entry(alias.clone()).or_insert(directive.clone());
|
||||
for alias in config.aliases.iter() {
|
||||
inner.entry(alias.clone()).or_insert(config.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +145,13 @@ impl CustomDirectiveMap {
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
@@ -158,3 +169,10 @@ pub(crate) enum CssId {
|
||||
/// 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