Compare commits

..

26 Commits

Author SHA1 Message Date
Tom Milligan
2c18292401 fix: better errors on toml parsing failures 2024-05-24 10:38:39 +01:00
Tom Milligan
294af2478c Merge pull request #177 from tommilligan/cache-ci-more
ci: cache more cargo target dir
2024-05-19 17:38:27 +01:00
Tom Milligan
278d17792b Merge pull request #178 from tommilligan/fix-changelog
chore: fix changelog
2024-05-19 17:33:39 +01:00
Tom Milligan
9f6c73091a chore: fix changelog 2024-05-19 17:33:24 +01:00
Tom Milligan
9f221abc12 ci: cache more cargo target dir 2024-05-19 17:30:47 +01:00
Tom Milligan
82c7bd4fd9 Merge pull request #176 from tommilligan/prep-1.16.0
chore: prep 1.16.0 release
2024-05-19 17:20:49 +01:00
Tom Milligan
9bca2a66df chore: prep 1.16.0 release 2024-05-19 17:13:16 +01:00
Tom Milligan
80cce8480c Merge pull request #174 from yannickseurin/custom-collapsible
Allow to set the collapsible property for each directive
2024-05-19 17:03:37 +01:00
Yannick Seurin
5d5b73ded6 allow to set collapsible default value for each directive 2024-05-19 16:57:21 +01:00
Tom Milligan
c17a66440c Merge pull request #175 from tommilligan/update-toml
chore: dependency and msrv upgrades
2024-05-19 11:21:54 +01:00
Tom Milligan
068a375647 chore: dependency and msrv upgrades 2024-05-19 11:05:09 +01:00
Tom Milligan
f04016d017 Merge pull request #173 from yannickseurin/snake-case
remove serde kebab-case renaming for AdmonitionDefaults struct
2024-05-19 09:49:38 +01:00
Tom Milligan
3f8bf86ac3 fix: allow css_id_prefix kebab cases for backcompat 2024-05-19 09:43:48 +01:00
Yannick Seurin
8045e217c9 remove serde kebab-case renaming 2024-04-29 22:53:11 +02:00
Tom Milligan
85fde44c09 Merge pull request #170 from meator/git-repository-url
book: Add GitHub link
2024-03-20 11:41:40 +00:00
meator
a2c3e49ef9 book: Add GitHub link 2024-03-19 21:41:02 +01:00
Tom Milligan
8a0ecc5dd1 Merge pull request #167 from tommilligan/prep-1.15.0
chore: prep v1.15.0 release
2023-12-30 21:41:13 +01:00
Tom Milligan
467f5f8f13 chore: prep v1.15.0 release 2023-12-30 20:37:34 +00:00
Tom Milligan
c4a05dae9d Merge pull request #165 from tommilligan/99-custom-admonitions
feat: custom directives
2023-12-30 21:20:35 +01:00
Tom Milligan
1bb14684c4 chore: use LF everywhere in ci 2023-12-30 20:14:53 +00:00
Tom Milligan
b3798b4d9f feat: custom directives 2023-12-30 20:14:53 +00:00
Tom Milligan
26c344a1e6 Merge pull request #163 from tommilligan/fix-161
fix: additional-css UNIX style path normalization
2023-12-18 18:00:43 +01:00
Tom Milligan
b1e6a5ee1e Merge pull request #164 from tommilligan/contributing
chore: split contributing guide
2023-12-18 13:25:57 +01:00
Tom Milligan
0d7b64d1b8 chore: split contributing guide 2023-12-18 12:25:34 +00:00
carlocorradini
9730cd0aaa fix: additional-css UNIX style path normalization 2023-12-18 12:20:54 +00:00
Tom Milligan
dc219f755d Merge pull request #155 from tommilligan/prep-1.14.0
chore: prep v0.14.0 release
2023-11-16 01:04:35 +01:00
41 changed files with 2130 additions and 795 deletions

View File

@@ -7,6 +7,8 @@ jobs:
fast-test:
name: Fast test
runs-on: ubuntu-20.04
env:
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
steps:
- name: Checkout sources
uses: actions/checkout@v4
@@ -17,6 +19,7 @@ jobs:
~/.cargo/registry
~/.cargo/git
target
/tmp/cargo-install-target-dir
key: fast-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
- name: Install toolchain
uses: actions-rs/toolchain@v1
@@ -33,6 +36,8 @@ jobs:
needs: fast-test
name: Test main target
runs-on: ubuntu-20.04
env:
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
steps:
- name: Checkout sources
uses: actions/checkout@v4
@@ -44,7 +49,7 @@ jobs:
~/.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
@@ -72,26 +77,38 @@ jobs:
matrix:
os:
- ubuntu-20.04
# - windows-2019
rust:
- stable
- beta
- 1.66.0
- 1.74.0
experimental:
- false
# Run a canary test on nightly that's allowed to fail
include:
# Run a canary test on nightly that's allowed to fail
- os: ubuntu-20.04
rust: nightly
experimental: true
# Don't bother retesting stable linux, we did it in the comprehensive test
# Test only stable on Windows, presume we'd get same result on other
# versions as Linux
- os: windows-2022
rust: stable
experimental: false
exclude:
# Don't bother retesting stable linux, we did it in the comprehensive test
- os: ubuntu-20.04
rust: stable
experimental: false
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
env:
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
steps:
# This is required, otherwise we get files with CRLF on Windows
# Which causes tests relying on data loaded from files to fail
- name: Set git to use LF everywhere
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: Checkout sources
uses: actions/checkout@v4
- name: Cache build files
@@ -101,6 +118,7 @@ jobs:
~/.cargo/registry
~/.cargo/git
target
/tmp/cargo-install-target-dir
key: test-${{ matrix.os }}-${{ matrix.rust }}-cargo-${{ hashFiles('**/Cargo.toml') }}
- name: Install toolchain
uses: actions-rs/toolchain@v1

View File

@@ -37,6 +37,8 @@ jobs:
os: windows-latest
name: x86_64-pc-windows-msvc.zip
runs-on: ${{ matrix.os }}
env:
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
steps:
- name: Setup | Checkout
uses: actions/checkout@v4
@@ -48,6 +50,7 @@ jobs:
path: |
~/.cargo/registry
~/.cargo/git
/tmp/cargo-install-target-dir
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup | Rust
@@ -132,6 +135,8 @@ jobs:
name: Publish to crates.io
needs: github_release
runs-on: ubuntu-20.04
env:
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v3
@@ -141,7 +146,7 @@ jobs:
~/.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

View File

@@ -12,6 +12,8 @@ permissions:
jobs:
publish:
runs-on: ubuntu-20.04
env:
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v3
@@ -21,7 +23,7 @@ jobs:
~/.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

View File

@@ -1,6 +1,29 @@
# Changelog
## Unreleased
## v1.16.0
### Changed
- MSRV (minimum supported rust version) is now 1.74.0 ([#175](https://github.com/tommilligan/mdbook-admonish/pull/175))
- `custom` directives should now be configured under the `directive.custom` option. Existing `custom` configurations are supported for back compatibility ([#179](https://github.com/tommilligan/mdbook-admonish/pull/174))
### Added
- Make blocks `collapsible` on a per-directive basis. Thanks to [@yannickseurin](https://github.com/yannickseurin) for contributing this feature! ([#174](https://github.com/tommilligan/mdbook-admonish/pull/174))
### Fixed
- The `css_id_prefix` option now uses snake case for consistency (kebab case remains supported for back compatibility). Thanks to [@yannickseurin](https://github.com/yannickseurin) for fixing this! ([#173](https://github.com/tommilligan/mdbook-admonish/pull/173))
## 1.15.0
### Added
- Support [custom directives](https://tommilligan.github.io/mdbook-admonish/overview.html#custom-blocks) with the new `mdbook-admonish generate-custom` helper. See the [mdbook-admonish book](https://tommilligan.github.io/mdbook-admonish/overview.html#custom-blocks) for guidance. Thanks to [@Sky9x](https://github.com/Sky9x) for helping design this feature! ([#165](https://github.com/tommilligan/mdbook-admonish/pull/165))
### Fixed
- `additional-css` unix style path normalization. Thanks to [@carlocorradini](https://github.com/carlocorradini) for reporting and fixing! ([#163](https://github.com/tommilligan/mdbook-admonish/pull/163))
## 1.14.0

31
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,31 @@
# Contributing
## Workflow
Please submit a PR from a **new branch** in your fork.
Please do not submit a PR from your fork's `main` branch, as it makes collaborating on/editing the branch a pain.
## Project design
- Compiled CSS styles are built and committed from SCSS sources. See the `compile_assets` folder for details.
- `mdbook-admonish install` is responsible for delivering additional assets and configuration to a client book.
- `mdbook-admonish` is responsible for preprocessing book data, adding HTML that references compiled classnames.
## Scripts to get started
- `./scripts/install` installs other toolchains required for development
- `./scripts/check` runs a full CI check
- `./scripts/rebuild-book` rebuilds the reference book under `./book`. This is useful for integration testing locally.
## Making breaking changes in CSS
To make a breaking change in CSS, you should:
- Update the assets version in `./src/bin/assets/VERSION`
- Update the required assets version specifier in `./src/REQUIRED_ASSETS_VERSION`
You must make the next `mdbook-admonish` crate version at least a **minor** version bump.
## Releasing
Github workflows are setup such that pushing a `vX.Y.Z` tag will trigger a release to be cut.

1104
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
[package]
name = "mdbook-admonish"
version = "1.14.0"
version = "1.16.0"
edition = "2021"
rust-version = "1.66.0"
rust-version = "1.74.0"
authors = ["Tom Milligan <code@tommilligan.net>"]
description = "A preprocessor for mdbook to add Material Design admonishments."
@@ -30,21 +30,24 @@ anyhow = "1.0.75"
# Note: clap 4.4 increases MSRV to 1.70.0 (2023-06-01)
# To use MSRV supported dependencies, install using the lockfile with
# `cargo install mdbook-admonish --locked`
clap = { version = "4.3", default_features = false, features = ["std", "derive"], optional = true }
env_logger = { version = "0.10", default_features = false, optional = true }
clap = { version = "4.3", default-features = false, features = ["std", "derive"], optional = true }
env_logger = { version = "0.11", default-features = false, optional = true }
log = "0.4.20"
mdbook = "0.4.35"
once_cell = "1.18.0"
pulldown-cmark = "0.9.3"
path-slash = "0.2.1"
pulldown-cmark = "0.11"
regex = "1.9.6"
semver = "1.0.19"
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
# Peer dependency of mdbook
# 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 }
toml_edit = { version = "0.22.13", optional = true }
hex_color = { version = "3.0.0", features = ["serde"] }
[dev-dependencies]
pretty_assertions = "1.4.0"

View File

@@ -161,30 +161,7 @@ Guarantees provided are as follows:
## Development
Project design
- Compiled CSS styles are built and committed from SCSS sources. See the `compile_assets` folder for details.
- `mdbook-admonish install` is responsible for delivering additional assets and configuration to a client book.
- `mdbook-admonish` is responsible for preprocessing book data, adding HTML that references compiled classnames.
### Scripts to get started
- `./scripts/install` installs other toolchains required for development
- `./scripts/check` runs a full CI check
- `./scripts/rebuild-book` rebuilds the reference book under `./book`. This is useful for integration testing locally.
### Making breaking changes in CSS
To make a breaking change in CSS, you should:
- Update the assets version in `./src/bin/assets/VERSION`
- Update the required assets version specifier in `./src/REQUIRED_ASSETS_VERSION`
You must make the next `mdbook-admonish` crate version at least a **minor** version bump.
### Releasing
Github workflows are setup such that pushing a `vX.Y.Z` tag will trigger a release to be cut.
See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on developing.
## Thanks

View File

@@ -4,12 +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.0.2" # do not edit: managed by `mdbook-admonish install`
[preprocessor.admonish.directive.custom]
expensive = { icon = "./money-bag.svg", color = "#24ab38" }
[preprocessor.toc]
command = "mdbook-toc"
@@ -18,4 +22,4 @@ renderer = ["html"]
[output]
[output.html]
additional-css = ["./mdbook-admonish.css"]
additional-css = ["./mdbook-admonish.css", "./mdbook-admonish-custom.css"]

View File

@@ -0,0 +1,20 @@
:root {
--md-admonition-icon--admonish-expensive: url("data:image/svg+xml;charset=utf-8,<svg width='800px' height='800px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><!-- ref: https://www.svgrepo.com/svg/526038/money-bag --><!-- Used under CC Attribution License; Author: Solar Icons --><path fill-rule='evenodd' clip-rule='evenodd' d='M12.052 1.25H11.948C11.0495 1.24997 10.3003 1.24995 9.70552 1.32991C9.07773 1.41432 8.51093 1.59999 8.05546 2.05546C7.59999 2.51093 7.41432 3.07773 7.32991 3.70552C7.27259 4.13189 7.25637 5.15147 7.25179 6.02566C5.22954 6.09171 4.01536 6.32778 3.17157 7.17157C2 8.34315 2 10.2288 2 14C2 17.7712 2 19.6569 3.17157 20.8284C4.34314 22 6.22876 22 9.99998 22H14C17.7712 22 19.6569 22 20.8284 20.8284C22 19.6569 22 17.7712 22 14C22 10.2288 22 8.34315 20.8284 7.17157C19.9846 6.32778 18.7705 6.09171 16.7482 6.02566C16.7436 5.15147 16.7274 4.13189 16.6701 3.70552C16.5857 3.07773 16.4 2.51093 15.9445 2.05546C15.4891 1.59999 14.9223 1.41432 14.2945 1.32991C13.6997 1.24995 12.9505 1.24997 12.052 1.25ZM15.2479 6.00188C15.2434 5.15523 15.229 4.24407 15.1835 3.9054C15.1214 3.44393 15.0142 3.24644 14.8839 3.11612C14.7536 2.9858 14.5561 2.87858 14.0946 2.81654C13.6116 2.7516 12.964 2.75 12 2.75C11.036 2.75 10.3884 2.7516 9.90539 2.81654C9.44393 2.87858 9.24644 2.9858 9.11612 3.11612C8.9858 3.24644 8.87858 3.44393 8.81654 3.9054C8.771 4.24407 8.75661 5.15523 8.75208 6.00188C9.1435 6 9.55885 6 10 6H14C14.4412 6 14.8565 6 15.2479 6.00188ZM12 9.25C12.4142 9.25 12.75 9.58579 12.75 10V10.0102C13.8388 10.2845 14.75 11.143 14.75 12.3333C14.75 12.7475 14.4142 13.0833 14 13.0833C13.5858 13.0833 13.25 12.7475 13.25 12.3333C13.25 11.9493 12.8242 11.4167 12 11.4167C11.1758 11.4167 10.75 11.9493 10.75 12.3333C10.75 12.7174 11.1758 13.25 12 13.25C13.3849 13.25 14.75 14.2098 14.75 15.6667C14.75 16.857 13.8388 17.7155 12.75 17.9898V18C12.75 18.4142 12.4142 18.75 12 18.75C11.5858 18.75 11.25 18.4142 11.25 18V17.9898C10.1612 17.7155 9.25 16.857 9.25 15.6667C9.25 15.2525 9.58579 14.9167 10 14.9167C10.4142 14.9167 10.75 15.2525 10.75 15.6667C10.75 16.0507 11.1758 16.5833 12 16.5833C12.8242 16.5833 13.25 16.0507 13.25 15.6667C13.25 15.2826 12.8242 14.75 12 14.75C10.6151 14.75 9.25 13.7903 9.25 12.3333C9.25 11.143 10.1612 10.2845 11.25 10.0102V10C11.25 9.58579 11.5858 9.25 12 9.25Z' fill='%231C274C'/></svg>");
}
:is(.admonition):is(.admonish-expensive) {
border-color: #24ab38;
}
:is(.admonish-expensive) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(36, 171, 56, 0.1);
}
:is(.admonish-expensive) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #24ab38;
mask-image: var(--md-admonition-icon--admonish-expensive);
-webkit-mask-image: var(--md-admonition-icon--admonish-expensive);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}

6
book/money-bag.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!-- ref: https://www.svgrepo.com/svg/526038/money-bag -->
<!-- Used under CC Attribution License; Author: Solar Icons -->
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.052 1.25H11.948C11.0495 1.24997 10.3003 1.24995 9.70552 1.32991C9.07773 1.41432 8.51093 1.59999 8.05546 2.05546C7.59999 2.51093 7.41432 3.07773 7.32991 3.70552C7.27259 4.13189 7.25637 5.15147 7.25179 6.02566C5.22954 6.09171 4.01536 6.32778 3.17157 7.17157C2 8.34315 2 10.2288 2 14C2 17.7712 2 19.6569 3.17157 20.8284C4.34314 22 6.22876 22 9.99998 22H14C17.7712 22 19.6569 22 20.8284 20.8284C22 19.6569 22 17.7712 22 14C22 10.2288 22 8.34315 20.8284 7.17157C19.9846 6.32778 18.7705 6.09171 16.7482 6.02566C16.7436 5.15147 16.7274 4.13189 16.6701 3.70552C16.5857 3.07773 16.4 2.51093 15.9445 2.05546C15.4891 1.59999 14.9223 1.41432 14.2945 1.32991C13.6997 1.24995 12.9505 1.24997 12.052 1.25ZM15.2479 6.00188C15.2434 5.15523 15.229 4.24407 15.1835 3.9054C15.1214 3.44393 15.0142 3.24644 14.8839 3.11612C14.7536 2.9858 14.5561 2.87858 14.0946 2.81654C13.6116 2.7516 12.964 2.75 12 2.75C11.036 2.75 10.3884 2.7516 9.90539 2.81654C9.44393 2.87858 9.24644 2.9858 9.11612 3.11612C8.9858 3.24644 8.87858 3.44393 8.81654 3.9054C8.771 4.24407 8.75661 5.15523 8.75208 6.00188C9.1435 6 9.55885 6 10 6H14C14.4412 6 14.8565 6 15.2479 6.00188ZM12 9.25C12.4142 9.25 12.75 9.58579 12.75 10V10.0102C13.8388 10.2845 14.75 11.143 14.75 12.3333C14.75 12.7475 14.4142 13.0833 14 13.0833C13.5858 13.0833 13.25 12.7475 13.25 12.3333C13.25 11.9493 12.8242 11.4167 12 11.4167C11.1758 11.4167 10.75 11.9493 10.75 12.3333C10.75 12.7174 11.1758 13.25 12 13.25C13.3849 13.25 14.75 14.2098 14.75 15.6667C14.75 16.857 13.8388 17.7155 12.75 17.9898V18C12.75 18.4142 12.4142 18.75 12 18.75C11.5858 18.75 11.25 18.4142 11.25 18V17.9898C10.1612 17.7155 9.25 16.857 9.25 15.6667C9.25 15.2525 9.58579 14.9167 10 14.9167C10.4142 14.9167 10.75 15.2525 10.75 15.6667C10.75 16.0507 11.1758 16.5833 12 16.5833C12.8242 16.5833 13.25 16.0507 13.25 15.6667C13.25 15.2826 12.8242 14.75 12 14.75C10.6151 14.75 9.25 13.7903 9.25 12.3333C9.25 11.143 10.1612 10.2845 11.25 10.0102V10C11.25 9.58579 11.5858 9.25 12 9.25Z" fill="#1C274C"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -170,7 +170,7 @@ The default id is a normalized version of the admonishment's title,
prefixed with the `default.css_id_prefix`,
with an appended number if multiple blocks would have the same id.
Setting the `id` field will *ignore* all other ids and the duplicate counter.
Setting the `id` field will _ignore_ all other ids and the duplicate counter.
````
```admonish info title="My Info" id="my-special-info"
@@ -194,3 +194,46 @@ Will yield something like the following HTML, which you can then apply styles to
Content will be hidden initially.
```
### Custom blocks
You can add new block types via the `book.toml` config:
```toml
# book.toml
[[preprocessor.admonish.custom]]
directive = "expensive"
icon = "./money-bag.svg"
color = "#24ab38"
aliases = ["money", "cash", "budget"]
```
You must then generate the relevant CSS file, and reference it in the `output.html` section.
`mdbook-admonish` has a helper to quickly do this for you:
```bash
# Generates a file at ./mdbook-admonish-custom.css with your styles in
$ mdbook-admonish generate-custom ./mdbook-admonish-custom.css
```
```toml
# book.toml
[output.html]
# Reference the new file, so it's bundled in with book styles
additional-css = ["./mdbook-admonish.css", "./mdbook-admonish-custom.css"]
```
You can then reference the new directive (or alias) like usual in your blocks.
````
```admonish expensive
Remember, this operation costs money!
```
````
```admonish expensive
Remember, this operation costs money!
```
You can also set a default `title`. See the [Reference](./reference.md) page for more details.

View File

@@ -75,6 +75,53 @@ Subfields:
- For the `html` renderer, the default value is `html`.
- For all other renderers, the default value is `preserve`.
### `directive`
Optional.
Settings relating to each type of block.
#### `builtin`
Optional.
Override the settings of a builtin directive.
The subkey of `builtin` is the directive to override. This must be the first directive listed in the [Directives](#directives) section below, e.g. `warning` (not `caution` or other aliases).
```toml
[preprocessor.admonish.directive.builtin.warning]
collapsible = true
```
Subfields:
- `collapsible` (optional): The default boolean value of the collapsible property for this type of block.
#### `custom`
Optional.
Additional types of block to support. The subkey of `custom` is the new directive to support.
You must run `mdbook-admonish generate-custom` after updating these values, to generate the correct styles.
```toml
[preprocessor.admonish.directive.custom.expensive]
icon = "./money-bag.svg"
color = "#24ab38"
collapsible = true
aliases = ["money", "cash", "budget"]
```
Subfields:
- `icon`: A filepath relative to the book root to load an SVG icon from.
- `color`: An RGB hex encoded color to use for the icon.
- `collapsible` (optional): The default boolean value of the collapsible property for this type of block.
- `aliases` (optional): One or more alternative directives to use this block.
- `title` (optional): The default title for this type of block. If not specified, defaults to the directive in title case. To give each alias a custom title, add multiple custom blocks.
### `command`
Required.
@@ -93,6 +140,8 @@ This is automatically updated by `mdbook-admonish install` and should not be edi
All supported directives are listed below.
Custom directives can be added via the `custom` config option above.
`note`
```admonish note

View File

@@ -4,7 +4,9 @@
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "sass --no-source-map scss/mdbook-admonish.scss ../src/bin/assets/mdbook-admonish.css",
"build": "yarn run build-prod && yarn run build-custom-expected",
"build-prod": "sass --no-source-map scss/mdbook-admonish.scss ../src/bin/assets/mdbook-admonish.css",
"build-custom-expected": "sass --no-source-map scss/mdbook-admonish-custom-expected.scss ../src/test_data/mdbook-admonish-custom-expected.css",
"lint": "prettier --check .",
"fix": "prettier --write ."
},

View File

@@ -22,6 +22,7 @@
@use "sass:color";
@use "sass:list";
@use "./lib";
// ----------------------------------------------------------------------------
// Variables
@@ -68,17 +69,9 @@ $admonitions: (
) !default;
// ----------------------------------------------------------------------------
// Rules: layout
// ----------------------------------------------------------------------------
// Admonition variables
:root {
@each $names, $props in $admonitions {
--md-admonition-icon--#{nth($names, 1)}: url("data:image/svg+xml;charset=utf-8,#{nth($props, 2)}");
}
--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>");
}
// Static content - base for all admonitions
// ----------------------------------------------------------------------------
// Admonition
@@ -231,48 +224,20 @@ summary.admonition-title {
}
}
// ----------------------------------------------------------------------------
// Rules: flavours
// ----------------------------------------------------------------------------
@each $names, $props in $admonitions {
$name: list.nth($names, 1);
$tint: list.nth($props, 1);
// Admonition flavour selectors
$flavours: ();
@each $name in $names {
$flavours: list.join($flavours, ".#{$name}", $separator: comma);
}
// Admonition flavour
:is(.admonition):is(#{$flavours}) {
border-color: $tint;
}
// Admonition flavour title
:is(#{$flavours}) > :is(.admonition-title, summary.admonition-title) {
background-color: color.adjust($tint, $alpha: -0.9);
// Admonition icon
&::before {
background-color: $tint;
mask-image: var(--md-admonition-icon--#{$name});
-webkit-mask-image: var(--md-admonition-icon--#{$name});
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
}
:root {
--md-details-icon: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
}
// Generate rules for each specified admonition variant
@include lib.from-admonitions($admonitions);
// ----------------------------------------------------------------------------
// Rules: themes
// ----------------------------------------------------------------------------
//
// One rule per builtin theme in mdbook, overriding the default fg/bg if matched
//
// This must be after the variant CSS, so it can override they styles set there
.navy {
& :is(.admonition) {

View File

@@ -0,0 +1,52 @@
@use "sass:color";
@use "sass:list";
@mixin from-admonitions($admonitions) {
// ----------------------------------------------------------------------------
// Rules: layout
// ----------------------------------------------------------------------------
// Admonition variables
:root {
@each $names, $props in $admonitions {
--md-admonition-icon--#{nth($names, 1)}: url("data:image/svg+xml;charset=utf-8,#{nth($props, 2)}");
}
}
// ----------------------------------------------------------------------------
// Rules: flavours
// ----------------------------------------------------------------------------
@each $names, $props in $admonitions {
$name: list.nth($names, 1);
$tint: list.nth($props, 1);
// Admonition flavour selectors
$flavours: ();
@each $name in $names {
$flavours: list.join($flavours, ".#{$name}", $separator: comma);
}
// Admonition flavour
:is(.admonition):is(#{$flavours}) {
border-color: $tint;
}
// Admonition flavour title
:is(#{$flavours}) > :is(.admonition-title, summary.admonition-title) {
background-color: color.adjust($tint, $alpha: -0.9);
// Admonition icon
&::before {
background-color: $tint;
mask-image: var(--md-admonition-icon--#{$name});
-webkit-mask-image: var(--md-admonition-icon--#{$name});
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
}
}
}

View File

@@ -0,0 +1,16 @@
// This file aims to generate the subset of CSS specific to a single admonition directive.
//
// This is used for unit test data in the rust css generation module.
@use "sass:color";
@use "sass:list";
@use "./lib";
@import "./material-color";
$admonitions: (
admonish-note: $clr-blue-a200
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>",
) !default;
// Generate rules for each specified admonition variant
@include lib.from-admonitions($admonitions);

View File

@@ -9,13 +9,18 @@ title = "mdbook-admonish-integration"
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "3.0.1" # do not edit: managed by `mdbook-admonish install`
assets_version = "3.0.2" # do not edit: managed by `mdbook-admonish install`
after = ["links"]
[[preprocessor.admonish.custom]]
directive = "frog"
icon = "./frog.svg"
color = "#9004CC"
[preprocessor.admonish.renderer.test]
render_mode = "strip"
[output]
[output.html]
additional-css = ["./mdbook-admonish.css"]
additional-css = ["./mdbook-admonish.css", "./mdbook-admonish-custom.css"]

View File

@@ -9,13 +9,18 @@ title = "mdbook-admonish-integration"
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "3.0.1" # do not edit: managed by `mdbook-admonish install`
assets_version = "3.0.2" # do not edit: managed by `mdbook-admonish install`
after = ["links"]
[[preprocessor.admonish.custom]]
directive = "frog"
icon = "./frog.svg"
color = "#9004CC"
[preprocessor.admonish.renderer.test]
render_mode = "strip"
[output]
[output.html]
additional-css = ["./mdbook-admonish.css"]
additional-css = ["./mdbook-admonish.css", "./mdbook-admonish-custom.css"]

View File

@@ -18,6 +18,15 @@
<p>Simples</p>
</div>
</div>
<div id="admonition-frog" class="admonition admonish-frog">
<div class="admonition-title">
<p>Frog</p>
<p><a class="admonition-anchor-link" href="#admonition-frog"></a></p>
</div>
<div>
<p>Custom frog directive</p>
</div>
</div>
<div id="admonition-default" class="admonition admonish-warning">
<div>
<p>No title, only body</p>
@@ -30,7 +39,9 @@
</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=&quot;' is not a valid directive or TOML key-value pair.
TOML parsing error: TOML parse error at line 1, column 8
|
1 | title=&quot;
| ^

View File

@@ -0,0 +1,20 @@
:root {
--md-admonition-icon--admonish-frog: url("data:image/svg+xml;charset=utf-8,<svg height='800px' width='800px' version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' xml:space='preserve'><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><!-- ref: https://www.svgrepo.com/svg/232692/frog --><!-- Used under CC0 Licence --><path d='M472,268.109H40c-4.418,0-8,3.582-8,8s3.582,8,8,8h432c4.418,0,8-3.582,8-8S476.418,268.109,472,268.109z'/><path d='M320,300.109H192c-4.418,0-8,3.582-8,8s3.582,8,8,8h128c4.418,0,8-3.582,8-8S324.418,300.109,320,300.109z'/><path d='M483.105,224.949c-9.717-8.094-13.741-11.93-11.921-21.398c5.588-10.328,8.816-22.464,8.816-35.441c0-21.712-9.036-41.065-23.058-53.523c-8.67-14.834-24.283-25.463-43.124-29.246c-20.05-4.027-40.644,0.458-55.09,11.994C321.682,126.917,290,140.109,256,140.109s-65.682-13.192-102.728-42.776c-14.446-11.535-35.04-16.02-55.09-11.994c-18.841,3.783-34.454,14.412-43.124,29.246C41.036,127.044,32,146.397,32,168.109c0,12.977,3.228,25.113,8.816,35.441c1.82,9.468-2.204,13.305-11.921,21.398C16.701,235.106,0,249.018,0,284.109c0,30.939,44.4,68.017,85.951,92.4c55.883,32.792,117.864,51.6,170.049,51.6s114.166-18.808,170.049-51.6C467.6,352.126,512,315.049,512,284.109C512,249.018,495.299,235.106,483.105,224.949z M464,168.109c0,10.585-2.696,20.437-7.312,28.661c-0.078,0.131-0.148,0.266-0.219,0.402c-7.916,13.829-21.31,22.937-36.47,22.937c-24.262,0-44-23.327-44-52s19.738-52,44-52c9.282,0,17.896,3.423,25.002,9.245c0.351,0.356,0.731,0.674,1.135,0.955C456.965,135.79,464,150.995,464,168.109z M65.857,126.314c0.407-0.282,0.791-0.603,1.144-0.962c7.105-5.82,15.718-9.242,24.999-9.242c24.262,0,44,23.327,44,52s-19.738,52-44,52c-15.159,0-28.552-9.107-36.468-22.935c-0.072-0.138-0.143-0.275-0.222-0.407C50.696,188.545,48,178.694,48,168.11C48,150.999,55.032,135.795,65.857,126.314z M417.951,362.709c-52.713,30.933-113.256,49.4-161.951,49.4s-109.238-18.467-161.951-49.4C48.095,335.744,16,303.422,16,284.109c0-27.596,11.761-37.393,23.135-46.867c5.547-4.621,11.494-9.583,15.006-16.426c10.334,9.552,23.514,15.293,37.858,15.293c33.084,0,60-30.505,60-68c0-33.803-21.879-61.914-50.429-67.125c15.396-3.025,30.975,0.273,41.717,8.852c40.075,32.002,74.837,46.273,112.712,46.273s72.637-14.271,112.712-46.273c10.743-8.579,26.321-11.878,41.717-8.852C381.879,106.195,360,134.307,360,168.109c0,37.495,26.916,68,60,68c14.344,0,27.524-5.741,37.858-15.293c3.512,6.842,9.459,11.805,15.006,16.426C484.239,246.717,496,256.513,496,284.109C496,303.422,463.905,335.744,417.951,362.709z'/><path d='M92,180.109c8.445,0,36-1.154,36-16s-27.555-16-36-16s-36,1.154-36,16S83.555,180.109,92,180.109z'/><path d='M384,164.109c0,14.846,27.555,16,36,16s36-1.154,36-16s-27.555-16-36-16S384,149.263,384,164.109z'/><path d='M232,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C224,224.528,227.582,228.109,232,228.109z'/><path d='M280,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C272,224.528,275.582,228.109,280,228.109z'/></svg>");
}
:is(.admonition):is(.admonish-frog) {
border-color: #9004cc;
}
:is(.admonish-frog) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(144, 4, 204, 0.1);
}
:is(.admonish-frog) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #9004cc;
mask-image: var(--md-admonition-icon--admonish-frog);
-webkit-mask-image: var(--md-admonition-icon--admonish-frog);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}

29
integration/frog.svg Normal file
View File

@@ -0,0 +1,29 @@
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!-- ref: https://www.svgrepo.com/svg/232692/frog -->
<!-- Used under CC0 Licence -->
<path d="M472,268.109H40c-4.418,0-8,3.582-8,8s3.582,8,8,8h432c4.418,0,8-3.582,8-8S476.418,268.109,472,268.109z"/>
<path d="M320,300.109H192c-4.418,0-8,3.582-8,8s3.582,8,8,8h128c4.418,0,8-3.582,8-8S324.418,300.109,320,300.109z"/>
<path d="M483.105,224.949c-9.717-8.094-13.741-11.93-11.921-21.398c5.588-10.328,8.816-22.464,8.816-35.441
c0-21.712-9.036-41.065-23.058-53.523c-8.67-14.834-24.283-25.463-43.124-29.246c-20.05-4.027-40.644,0.458-55.09,11.994
C321.682,126.917,290,140.109,256,140.109s-65.682-13.192-102.728-42.776c-14.446-11.535-35.04-16.02-55.09-11.994
c-18.841,3.783-34.454,14.412-43.124,29.246C41.036,127.044,32,146.397,32,168.109c0,12.977,3.228,25.113,8.816,35.441
c1.82,9.468-2.204,13.305-11.921,21.398C16.701,235.106,0,249.018,0,284.109c0,30.939,44.4,68.017,85.951,92.4
c55.883,32.792,117.864,51.6,170.049,51.6s114.166-18.808,170.049-51.6C467.6,352.126,512,315.049,512,284.109
C512,249.018,495.299,235.106,483.105,224.949z M464,168.109c0,10.585-2.696,20.437-7.312,28.661
c-0.078,0.131-0.148,0.266-0.219,0.402c-7.916,13.829-21.31,22.937-36.47,22.937c-24.262,0-44-23.327-44-52s19.738-52,44-52
c9.282,0,17.896,3.423,25.002,9.245c0.351,0.356,0.731,0.674,1.135,0.955C456.965,135.79,464,150.995,464,168.109z M65.857,126.314
c0.407-0.282,0.791-0.603,1.144-0.962c7.105-5.82,15.718-9.242,24.999-9.242c24.262,0,44,23.327,44,52s-19.738,52-44,52
c-15.159,0-28.552-9.107-36.468-22.935c-0.072-0.138-0.143-0.275-0.222-0.407C50.696,188.545,48,178.694,48,168.11
C48,150.999,55.032,135.795,65.857,126.314z M417.951,362.709c-52.713,30.933-113.256,49.4-161.951,49.4
s-109.238-18.467-161.951-49.4C48.095,335.744,16,303.422,16,284.109c0-27.596,11.761-37.393,23.135-46.867
c5.547-4.621,11.494-9.583,15.006-16.426c10.334,9.552,23.514,15.293,37.858,15.293c33.084,0,60-30.505,60-68
c0-33.803-21.879-61.914-50.429-67.125c15.396-3.025,30.975,0.273,41.717,8.852c40.075,32.002,74.837,46.273,112.712,46.273
s72.637-14.271,112.712-46.273c10.743-8.579,26.321-11.878,41.717-8.852C381.879,106.195,360,134.307,360,168.109
c0,37.495,26.916,68,60,68c14.344,0,27.524-5.741,37.858-15.293c3.512,6.842,9.459,11.805,15.006,16.426
C484.239,246.717,496,256.513,496,284.109C496,303.422,463.905,335.744,417.951,362.709z"/>
<path d="M92,180.109c8.445,0,36-1.154,36-16s-27.555-16-36-16s-36,1.154-36,16S83.555,180.109,92,180.109z"/>
<path d="M384,164.109c0,14.846,27.555,16,36,16s36-1.154,36-16s-27.555-16-36-16S384,149.263,384,164.109z"/>
<path d="M232,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C224,224.528,227.582,228.109,232,228.109z"/>
<path d="M280,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C272,224.528,275.582,228.109,280,228.109z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,20 @@
:root {
--md-admonition-icon--admonish-frog: url("data:image/svg+xml;charset=utf-8,<svg height='800px' width='800px' version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 512 512' xml:space='preserve'><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><!-- ref: https://www.svgrepo.com/svg/232692/frog --><!-- Used under CC0 Licence --><path d='M472,268.109H40c-4.418,0-8,3.582-8,8s3.582,8,8,8h432c4.418,0,8-3.582,8-8S476.418,268.109,472,268.109z'/><path d='M320,300.109H192c-4.418,0-8,3.582-8,8s3.582,8,8,8h128c4.418,0,8-3.582,8-8S324.418,300.109,320,300.109z'/><path d='M483.105,224.949c-9.717-8.094-13.741-11.93-11.921-21.398c5.588-10.328,8.816-22.464,8.816-35.441c0-21.712-9.036-41.065-23.058-53.523c-8.67-14.834-24.283-25.463-43.124-29.246c-20.05-4.027-40.644,0.458-55.09,11.994C321.682,126.917,290,140.109,256,140.109s-65.682-13.192-102.728-42.776c-14.446-11.535-35.04-16.02-55.09-11.994c-18.841,3.783-34.454,14.412-43.124,29.246C41.036,127.044,32,146.397,32,168.109c0,12.977,3.228,25.113,8.816,35.441c1.82,9.468-2.204,13.305-11.921,21.398C16.701,235.106,0,249.018,0,284.109c0,30.939,44.4,68.017,85.951,92.4c55.883,32.792,117.864,51.6,170.049,51.6s114.166-18.808,170.049-51.6C467.6,352.126,512,315.049,512,284.109C512,249.018,495.299,235.106,483.105,224.949z M464,168.109c0,10.585-2.696,20.437-7.312,28.661c-0.078,0.131-0.148,0.266-0.219,0.402c-7.916,13.829-21.31,22.937-36.47,22.937c-24.262,0-44-23.327-44-52s19.738-52,44-52c9.282,0,17.896,3.423,25.002,9.245c0.351,0.356,0.731,0.674,1.135,0.955C456.965,135.79,464,150.995,464,168.109z M65.857,126.314c0.407-0.282,0.791-0.603,1.144-0.962c7.105-5.82,15.718-9.242,24.999-9.242c24.262,0,44,23.327,44,52s-19.738,52-44,52c-15.159,0-28.552-9.107-36.468-22.935c-0.072-0.138-0.143-0.275-0.222-0.407C50.696,188.545,48,178.694,48,168.11C48,150.999,55.032,135.795,65.857,126.314z M417.951,362.709c-52.713,30.933-113.256,49.4-161.951,49.4s-109.238-18.467-161.951-49.4C48.095,335.744,16,303.422,16,284.109c0-27.596,11.761-37.393,23.135-46.867c5.547-4.621,11.494-9.583,15.006-16.426c10.334,9.552,23.514,15.293,37.858,15.293c33.084,0,60-30.505,60-68c0-33.803-21.879-61.914-50.429-67.125c15.396-3.025,30.975,0.273,41.717,8.852c40.075,32.002,74.837,46.273,112.712,46.273s72.637-14.271,112.712-46.273c10.743-8.579,26.321-11.878,41.717-8.852C381.879,106.195,360,134.307,360,168.109c0,37.495,26.916,68,60,68c14.344,0,27.524-5.741,37.858-15.293c3.512,6.842,9.459,11.805,15.006,16.426C484.239,246.717,496,256.513,496,284.109C496,303.422,463.905,335.744,417.951,362.709z'/><path d='M92,180.109c8.445,0,36-1.154,36-16s-27.555-16-36-16s-36,1.154-36,16S83.555,180.109,92,180.109z'/><path d='M384,164.109c0,14.846,27.555,16,36,16s36-1.154,36-16s-27.555-16-36-16S384,149.263,384,164.109z'/><path d='M232,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C224,224.528,227.582,228.109,232,228.109z'/><path d='M280,228.109c4.418,0,8-3.582,8-8v-8c0-4.418-3.582-8-8-8s-8,3.582-8,8v8C272,224.528,275.582,228.109,280,228.109z'/></svg>");
}
:is(.admonition):is(.admonish-frog) {
border-color: #9004cc;
}
:is(.admonish-frog) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(144, 4, 204, 0.1);
}
:is(.admonish-frog) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #9004cc;
mask-image: var(--md-admonition-icon--admonish-frog);
-webkit-mask-image: var(--md-admonition-icon--admonish-frog);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}

View File

@@ -34,6 +34,27 @@ if [ "$DIFF_RESULT" != 0 ]; then
exit 1
fi
eprintln "Generating custom CSS (book)"
mdbook-admonish generate-custom mdbook-admonish-custom.css
eprintln "Verifying generated book custom css"
set +e
diff -u \
"expected/mdbook-admonish-custom.css" \
"./mdbook-admonish-custom.css"
DIFF_RESULT=$?
set -e
if [ "$DIFF_RESULT" != 0 ]; then
eprintln ""
eprintln "error: generated custom css was different than expected"
eprintln ""
eprintln "error: If you expected the output to change, run:"
eprintln "cp ./integration/mdbook-admonish-custom.css ./integration/expected/mdbook-admonish-custom.css"
eprintln "and commit the result"
exit 1
fi
eprintln "Building mdbook"
mdbook build

View File

@@ -10,6 +10,10 @@ It verifies that `mdbook` post-processes our generated HTML in the way we expect
Simples
```
```admonish frog
Custom frog directive
```
```admonish warning ""
No title, only body
```

View File

@@ -18,7 +18,20 @@ eprintln "Checking compiled styles up to date"
COMITTED_ASSETS="$(cat ../src/bin/assets/mdbook-admonish.css)"
yarn run build
RECOMPILED_ASSETS="$(cat ../src/bin/assets/mdbook-admonish.css)"
set +e
diff -u <(printf "%s" "$COMITTED_ASSETS") <(printf "%s" "$RECOMPILED_ASSETS")
DIFF_RESULT=$?
set -e
if [ "$DIFF_RESULT" != 0 ]; then
eprintln ""
eprintln "error: committed assets are not up to date"
eprintln ""
eprintln "error: To update committed assets, please run"
eprintln "cd compile_assets && yarn run build"
eprintln "and commit the result"
exit 1
fi
popd > /dev/null

View File

@@ -1 +1 @@
3.0.1
3.0.2

View File

@@ -1,20 +1,4 @@
@charset "UTF-8";
:root {
--md-admonition-icon--admonish-note: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
--md-admonition-icon--admonish-abstract: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
--md-admonition-icon--admonish-info: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
--md-admonition-icon--admonish-tip: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
--md-admonition-icon--admonish-success: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
--md-admonition-icon--admonish-question: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
--md-admonition-icon--admonish-warning: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
--md-admonition-icon--admonish-failure: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
--md-admonition-icon--admonish-danger: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
--md-admonition-icon--admonish-bug: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
--md-admonition-icon--admonish-example: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z'/></svg>");
--md-admonition-icon--admonish-quote: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>");
--md-details-icon: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
}
:is(.admonition) {
display: flow-root;
margin: 1.5625em 0;
@@ -123,6 +107,25 @@ details[open].admonition > summary.admonition-title::after {
transform: rotate(90deg);
}
:root {
--md-details-icon: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
}
:root {
--md-admonition-icon--admonish-note: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
--md-admonition-icon--admonish-abstract: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
--md-admonition-icon--admonish-info: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
--md-admonition-icon--admonish-tip: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
--md-admonition-icon--admonish-success: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
--md-admonition-icon--admonish-question: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
--md-admonition-icon--admonish-warning: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
--md-admonition-icon--admonish-failure: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
--md-admonition-icon--admonish-danger: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
--md-admonition-icon--admonish-bug: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
--md-admonition-icon--admonish-example: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z'/></svg>");
--md-admonition-icon--admonish-quote: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>");
}
:is(.admonition):is(.admonish-note) {
border-color: #448aff;
}

View File

@@ -1,13 +1,12 @@
use anyhow::Result;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use mdbook::{
errors::Error,
preprocess::{CmdPreprocessor, Preprocessor},
};
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
use mdbook_admonish::Admonish;
#[cfg(feature = "cli-install")]
use serde::Deserialize;
use std::fs;
use std::io;
use std::path::PathBuf;
use std::{io, process};
use std::process;
/// mdbook preprocessor to add support for admonitions
#[derive(Parser)]
@@ -36,6 +35,18 @@ enum Commands {
#[arg(long)]
css_dir: Option<PathBuf>,
},
/// Generate CSS file for custom directives.
GenerateCustom {
/// Root directory for the book, should contain the configuration file (`book.toml`)
///
/// If not set, defaults to the current directory.
#[arg(long)]
dir: Option<PathBuf>,
/// File to write generated css to.
output: PathBuf,
},
}
fn main() {
@@ -62,10 +73,13 @@ fn run(cli: Cli) -> Result<()> {
dir.unwrap_or_else(|| PathBuf::from(".")),
css_dir.unwrap_or_else(|| PathBuf::from(".")),
),
Some(Commands::GenerateCustom { dir, output }) => {
handle_generate_custom(dir.unwrap_or_else(|| PathBuf::from(".")), output)
}
}
}
fn handle_preprocessing() -> Result<(), Error> {
fn handle_preprocessing() -> std::result::Result<(), mdbook::errors::Error> {
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
@@ -94,15 +108,59 @@ fn handle_supports(renderer: String) -> ! {
}
}
#[derive(Deserialize)]
struct Config {
#[serde(default)]
preprocessor: Preprocessors,
}
#[derive(Default, Deserialize)]
struct Preprocessors {
#[serde(default)]
admonish: Option<toml::Table>,
#[serde(flatten)]
_other: toml::Table,
}
/// Load the plugin specific config as a toml string, for private deserialization.
fn admonish_config_string(config: &Config) -> Result<String> {
Ok(toml_mdbook::to_string(
&config
.preprocessor
.admonish
.as_ref()
.context("No configuration for mdbook-admonish in book.toml")?,
)?)
}
fn handle_generate_custom(proj_dir: PathBuf, output: PathBuf) -> Result<()> {
let config = proj_dir.join("book.toml");
log::info!("Reading configuration file '{}'", config.display());
let data = fs::read_to_string(&config)
.with_context(|| format!("can't read configuration file '{}'", config.display()))?;
let config: Config = toml::from_str(&data).context("Invalid configuration file")?;
let css =
mdbook_admonish::custom::css_from_config(&proj_dir, &admonish_config_string(&config)?)?;
log::info!("Writing custom CSS file '{}'", output.display());
fs::write(output, css)?;
Ok(())
}
#[cfg(feature = "cli-install")]
mod install {
use anyhow::{Context, Result};
use path_slash::PathExt;
use std::borrow::Cow;
use std::path::Path;
use std::{
fs::{self, File},
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",
@@ -122,13 +180,23 @@ mod install {
}
}
// Normalize path to UNIX style.
// This avoids conflicts/rewriting files when projects are used under different
// operating systems (e.g. on Windows, after being used on Linux)
//
// https://github.com/tommilligan/mdbook-admonish/issues/161
fn normalize_config_file_path(path: &Path) -> Result<Cow<'_, str>> {
path.to_slash()
.context("UNIX style path normalization error")
}
pub fn handle_install(proj_dir: PathBuf, css_dir: PathBuf) -> Result<()> {
let config = proj_dir.join("book.toml");
log::info!("Reading configuration file '{}'", config.display());
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) {
@@ -139,7 +207,7 @@ mod install {
);
preprocessor["assets_version"] = value;
} else {
log::info!("Unexpected configuration, not updating prereprocessor configuration");
log::info!("Unexpected configuration, not updating preprocessor configuration");
};
let mut additional_css = additional_css(&mut doc);
@@ -148,12 +216,13 @@ mod install {
// Normalize path to remove no-op components
// https://github.com/tommilligan/mdbook-admonish/issues/47
let filepath: PathBuf = filepath.components().collect();
let filepath_str = filepath.to_str().context("non-utf8 filepath")?;
if let Ok(ref mut additional_css) = additional_css {
if !additional_css.contains_str(filepath_str) {
let filepath_str = normalize_config_file_path(&filepath)?;
if !additional_css.contains_str(&filepath_str) {
log::info!("Adding '{filepath_str}' to 'additional-css'");
additional_css.push(filepath_str);
additional_css.push(filepath_str.as_ref());
}
} else {
log::warn!("Unexpected configuration, not updating 'additional-css'");
@@ -190,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());
@@ -214,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());
@@ -227,4 +296,24 @@ A beautifully styled message.
item["command"] = toml_edit::value("mdbook-admonish");
Ok(item)
}
#[cfg(test)]
mod test {
use super::*;
/// This test seems redundant, but would fail on Windows.
///
/// We want to always convert to a fixed output string style, independant
/// of runtime platform, and forward slashes in relative paths are fine on
/// Windows.
#[test]
fn test_normalize_config_file_path() {
let input = PathBuf::from(".")
.join("css-dir")
.join("mdbook-admonish.css");
let expected = "./css-dir/mdbook-admonish.css";
let actual = normalize_config_file_path(&input).unwrap();
assert_eq!(actual.as_ref(), expected);
}
}
}

View File

@@ -2,8 +2,9 @@ use anyhow::{Context, Result};
use mdbook::preprocess::PreprocessorContext;
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.
///
@@ -15,10 +16,47 @@ pub(crate) fn admonish_config_from_context(ctx: &PreprocessorContext) -> Result<
.get_preprocessor("admonish")
.context("No configuration for mdbook-admonish in book.toml")?,
)?;
toml::from_str(&table).context("Invalid mdbook-admonish configuration in book.toml")
admonish_config_from_str(&table)
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) fn admonish_config_from_str(data: &str) -> Result<Config> {
let readonly: ConfigReadonly =
toml::from_str(data).context("Invalid mdbook-admonish configuration in book.toml")?;
let config = readonly.into();
log::debug!("Loaded admonish config: {:?}", config);
Ok(config)
}
/// All valid input states including back-compatibility fields.
///
/// This struct deliberately does not implement Serialize as it never meant to
/// be written, only converted to Config.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
struct ConfigReadonly {
#[serde(default)]
pub on_failure: OnFailure,
#[serde(default)]
pub default: AdmonitionDefaults,
#[serde(default)]
pub renderer: HashMap<String, RendererConfig>,
#[serde(default)]
pub assets_version: Option<String>,
#[serde(default)]
pub custom: Vec<CustomDirectiveReadonly>,
#[serde(default)]
pub builtin: HashMap<BuiltinDirective, BuiltinDirectiveConfig>,
#[serde(default)]
pub directive: DirectiveConfig,
}
/// The canonical config format, without back-compatibility
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub(crate) struct Config {
#[serde(default)]
pub on_failure: OnFailure,
@@ -31,6 +69,79 @@ pub(crate) struct Config {
#[serde(default)]
pub assets_version: Option<String>,
#[serde(default)]
pub directive: DirectiveConfig,
}
impl From<ConfigReadonly> for Config {
fn from(other: ConfigReadonly) -> Self {
let ConfigReadonly {
on_failure,
default,
renderer,
assets_version,
custom,
builtin,
mut directive,
} = other;
// Merge deprecated config fields into main config object
directive.custom.extend(
custom
.into_iter()
.map(|CustomDirectiveReadonly { directive, config }| (directive, config)),
);
directive.builtin.extend(builtin);
Self {
on_failure,
default,
renderer,
assets_version,
directive,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub(crate) struct DirectiveConfig {
#[serde(default)]
pub custom: HashMap<String, CustomDirective>,
#[serde(default)]
pub builtin: HashMap<BuiltinDirective, BuiltinDirectiveConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct CustomDirective {
/// Path to an SVG file, relative to the book root.
pub icon: PathBuf,
/// Primary color for this directive.
pub color: hex_color::HexColor,
/// Alternative directives the user can specify
#[serde(default)]
pub aliases: Vec<String>,
/// Title to use, human readable.
#[serde(default)]
pub title: Option<String>,
/// Default collapsible value.
#[serde(default)]
pub collapsible: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct CustomDirectiveReadonly {
/// The primary directive. Used for CSS classnames
pub directive: String,
/// Path to an SVG file, relative to the book root.
#[serde(flatten)]
config: CustomDirective,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
@@ -58,3 +169,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(())
}
}

View File

@@ -21,10 +21,8 @@ struct UserInput {
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!(
@@ -52,7 +50,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,
@@ -66,12 +64,14 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig,
Lazy::new(|| Regex::new(r#"^[A-Za-z0-9_-]+$"#).expect("directive regex"));
if !RX_DIRECTIVE.is_match(directive) {
return original_error;
return Err(format!("'{directive}' is not a valid directive or TOML key-value pair.\n\n{original_error}"));
}
let mut config: UserInput = match toml::from_str(config_toml) {
Ok(config) => config,
Err(_) => return original_error,
Err(error) => {
return Err(format!("TOML parsing error: {error}"));
}
};
config.r#type = Some(directive.to_owned());
config
@@ -102,97 +102,119 @@ 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
"#
);
}

111
src/custom.rs Normal file
View File

@@ -0,0 +1,111 @@
//! This module is responsible for generating custom CSS for new admonition variants.
//!
//! It has unit tests to ensure the output matches that of the compile_assets CSS.
use anyhow::{anyhow, Context, Result};
use hex_color::{Case, HexColor};
use once_cell::sync::Lazy;
use regex::Regex;
use std::fs;
use std::path::Path;
static RX_COLLAPSE_NEWLINES: Lazy<Regex> =
Lazy::new(|| Regex::new(r"[\r\n]+\s*").expect("invalid whitespace regex"));
// Do some simple things to make the svg input probably a valid data url
// Based on this gist: https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
fn svg_to_data_url(svg: &str) -> String {
const XMLNS: &str = r#"http://www.w3.org/2000/svg"#;
//
let mut svg = RX_COLLAPSE_NEWLINES.replace_all(svg, "").to_string();
if !svg.contains(XMLNS) {
log::warn!("Your SVG file does not contain '<svg xmlns=\"{XMLNS}\"', it will likely fail to render.");
}
svg = svg
.replace('"', "'")
.replace('%', "%25")
.replace('#', "%23")
.replace('{', "%7B")
.replace('}', "%7D");
format!("url(\"data:image/svg+xml;charset=utf-8,{}\")", svg)
}
/// Given a valid set of inputs, generate the relevant CSS.
///
/// It is up to the caller to validate inputs.
fn directive_css(name: &str, svg_data: &str, tint: HexColor) -> String {
let data_url = svg_to_data_url(svg_data);
let tint_faint = format!("rgba({}, {}, {}, {})", tint.r, tint.g, tint.b, 0.1);
let tint = tint.display_rgb().with_case(Case::Lower);
format!(
":root {{
--md-admonition-icon--admonish-{name}: {data_url};
}}
:is(.admonition):is(.admonish-{name}) {{
border-color: {tint};
}}
:is(.admonish-{name}) > :is(.admonition-title, summary.admonition-title) {{
background-color: {tint_faint};
}}
:is(.admonish-{name}) > :is(.admonition-title, summary.admonition-title)::before {{
background-color: {tint};
mask-image: var(--md-admonition-icon--admonish-{name});
-webkit-mask-image: var(--md-admonition-icon--admonish-{name});
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}}
",
name = name,
data_url = data_url,
tint = tint,
tint_faint = tint_faint
)
}
#[doc(hidden)]
pub fn css_from_config(book_dir: &Path, config: &str) -> Result<String> {
let config = crate::book_config::admonish_config_from_str(config)?;
let custom_directives = config.directive.custom;
if custom_directives.is_empty() {
return Err(anyhow!("No custom directives provided"));
}
log::info!("Loaded {} custom directives", custom_directives.len());
let mut css = String::new();
for (directive_name, directive) in custom_directives.iter() {
let svg = fs::read_to_string(book_dir.join(&directive.icon))
.with_context(|| format!("can't read icon file '{}'", directive.icon.display()))?;
css.push_str(&directive_css(directive_name, &svg, directive.color));
}
Ok(css)
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
const GENERATED_CSS: &str = include_str!("./test_data/mdbook-admonish-custom-expected.css");
const NOTE_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox='0 0 24 24'>
<path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/>
</svg>
"#;
// Verify the generated CSS here against a sample from the compile_assets output.
//
// The ensures that any new custom CSS will be in line with official styles.
#[test]
fn verify_against_generated_css() {
let actual = directive_css("note", NOTE_SVG, HexColor::parse("#448aff").unwrap());
assert_eq!(
GENERATED_CSS, actual,
"Rust generated CSS is out of step with SCSS generated CSS"
)
}
}

View File

@@ -1,5 +1,11 @@
//! For usage and reference, see the [mdbook-admonish book](https://tommilligan.github.io/mdbook-admonish/)
//!
//! Documentation is hosted externally, as docs.rs does not currently support plugins.
mod book_config;
mod config;
#[doc(hidden)]
pub mod custom;
mod markdown;
mod parse;
mod preprocessor;

View File

@@ -1,17 +1,16 @@
use mdbook::errors::Result as MdbookResult;
use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag};
pub use crate::preprocessor::Admonish;
use crate::{
book_config::OnFailure,
parse::parse_admonition,
types::{AdmonitionDefaults, RenderTextMode},
types::{Overrides, RenderTextMode},
};
pub(crate) fn preprocess(
content: &str,
on_failure: OnFailure,
admonition_defaults: &AdmonitionDefaults,
overrides: &Overrides,
render_text_mode: RenderTextMode,
) -> MdbookResult<String> {
let mut id_counter = Default::default();
@@ -33,7 +32,7 @@ pub(crate) fn preprocess(
let admonition = match parse_admonition(
info_string.as_ref(),
admonition_defaults,
overrides,
span_content,
on_failure,
indent,
@@ -91,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 [
@@ -136,7 +138,7 @@ mod test {
preprocess(
content,
OnFailure::Continue,
&AdmonitionDefaults::default(),
&Overrides::default(),
RenderTextMode::Html,
)
.unwrap()
@@ -594,6 +596,8 @@ Error rendering admonishment
Failed with:
```log
'title="' is not a valid directive or TOML key-value pair.
TOML parsing error: TOML parse error at line 1, column 8
|
1 | title="
@@ -629,7 +633,7 @@ Bonus content!
preprocess(
content,
OnFailure::Bail,
&AdmonitionDefaults::default(),
&Overrides::default(),
RenderTextMode::Html
)
.unwrap_err()
@@ -656,7 +660,7 @@ x = 20;
preprocess(
content,
OnFailure::Bail,
&AdmonitionDefaults::default(),
&Overrides::default(),
RenderTextMode::Strip
)
.unwrap(),
@@ -730,10 +734,13 @@ 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()
},
RenderTextMode::Html,
)
@@ -765,10 +772,13 @@ 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()
},
RenderTextMode::Html,
)
@@ -920,10 +930,13 @@ 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()
},
RenderTextMode::Html,
)
@@ -961,10 +974,13 @@ 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()
},
RenderTextMode::Html,
)
@@ -1002,10 +1018,13 @@ 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()
},
RenderTextMode::Html,
)

View File

@@ -1,12 +1,11 @@
use anyhow::{anyhow, Result};
use std::borrow::Cow;
pub use crate::preprocessor::Admonish;
use crate::{
book_config::OnFailure,
render::Admonition,
resolve::AdmonitionMeta,
types::{AdmonitionDefaults, CssId, Directive},
types::{BuiltinDirective, CssId, Overrides},
};
/// Given the content in the span of the code block, and the info string,
@@ -20,7 +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,
overrides: &'a Overrides,
content: &'a str,
on_failure: OnFailure,
indent: usize,
@@ -28,7 +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)?;
let info = AdmonitionMeta::from_info_string(info_string, overrides)?;
let info = match info {
Ok(info) => info,
Err(message) => {
@@ -44,7 +43,7 @@ pub(crate) fn parse_admonition<'a>(
r#"Error processing admonition. To fail the build instead of continuing, set 'on_failure = "bail"'"#
);
Ok(Admonition {
directive: Directive::Bug,
directive: BuiltinDirective::Bug.to_string(),
title: "Error rendering admonishment".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),

View File

@@ -8,7 +8,7 @@ use mdbook::{
use crate::{
book_config::{admonish_config_from_context, Config, RenderMode},
markdown::preprocess,
types::RenderTextMode,
types::{Overrides, RenderTextMode},
};
pub struct Admonish;
@@ -22,9 +22,21 @@ impl Preprocessor for Admonish {
let config = admonish_config_from_context(ctx)?;
ensure_compatible_assets_version(&config)?;
let custom_directives = config
.directive
.custom
.into_iter()
.map(Into::into)
.collect();
let on_failure = config.on_failure;
let admonition_defaults = config.default;
let overrides = Overrides {
book: admonition_defaults,
custom: custom_directives,
builtin: config.directive.builtin,
};
// Load what rendering we should do from config, falling back to a default
let render_mode = config
.renderer
@@ -53,15 +65,11 @@ impl Preprocessor for Admonish {
if let BookItem::Chapter(ref mut chapter) = *item {
res = Some(
preprocess(
&chapter.content,
on_failure,
&admonition_defaults,
render_text_mode,
)
.map(|md| {
chapter.content = md;
}),
preprocess(&chapter.content, on_failure, &overrides, render_text_mode).map(
|md| {
chapter.content = md;
},
),
);
}
});

View File

@@ -2,34 +2,11 @@ use mdbook::utils::unique_id_from_content;
use std::borrow::Cow;
use std::collections::HashMap;
pub use crate::preprocessor::Admonish;
use crate::{
resolve::AdmonitionMeta,
types::{CssId, Directive},
};
impl Directive {
fn classname(&self) -> &'static str {
match self {
Directive::Note => "admonish-note",
Directive::Abstract => "admonish-abstract",
Directive::Info => "admonish-info",
Directive::Tip => "admonish-tip",
Directive::Success => "admonish-success",
Directive::Question => "admonish-question",
Directive::Warning => "admonish-warning",
Directive::Failure => "admonish-failure",
Directive::Danger => "admonish-danger",
Directive::Bug => "admonish-bug",
Directive::Example => "admonish-example",
Directive::Quote => "admonish-quote",
}
}
}
use crate::{resolve::AdmonitionMeta, types::CssId};
#[derive(Debug, PartialEq)]
pub(crate) struct Admonition<'a> {
pub(crate) directive: Directive,
pub(crate) directive: String,
pub(crate) title: String,
pub(crate) content: Cow<'a, str>,
pub(crate) css_id: CssId,
@@ -75,7 +52,6 @@ impl<'a> Admonition<'a> {
}
};
let mut additional_class = Cow::Borrowed(self.directive.classname());
let title = &self.title;
let content = &self.content;
let indent = " ".repeat(self.indent);
@@ -96,14 +72,12 @@ impl<'a> Admonition<'a> {
Cow::Borrowed("")
};
let mut additional_class = format!("admonish-{}", self.directive);
if !self.additional_classnames.is_empty() {
let mut buffer = additional_class.into_owned();
for additional_classname in &self.additional_classnames {
buffer.push(' ');
buffer.push_str(additional_classname);
additional_class.push(' ');
additional_class.push_str(additional_classname);
}
additional_class = Cow::Owned(buffer);
}
let admonition_block = if self.collapsible { "details" } else { "div" };

View File

@@ -1,5 +1,6 @@
use crate::config::InstanceConfig;
use crate::types::{AdmonitionDefaults, CssId, Directive};
use crate::types::{BuiltinDirective, CssId, CustomDirective, CustomDirectiveMap, Overrides};
use std::fmt;
use std::str::FromStr;
/// All information required to render an admonition.
@@ -7,25 +8,64 @@ use std::str::FromStr;
/// i.e. all configured options have been resolved at this point.
#[derive(Debug, PartialEq)]
pub(crate) struct AdmonitionMeta {
pub directive: Directive,
pub directive: String,
pub title: String,
pub css_id: CssId,
pub additional_classnames: Vec<String>,
pub collapsible: bool,
}
/// Wrapper type to hold any value directive configuration.
enum Directive {
Builtin(BuiltinDirective),
Custom(CustomDirective),
}
impl fmt::Display for Directive {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Builtin(builtin) => builtin.fmt(f),
Self::Custom(custom) => f.write_str(&custom.directive),
}
}
}
impl Directive {
fn from_str(custom_directive_map: &CustomDirectiveMap, string: &str) -> Result<Self, ()> {
if let Ok(builtin) = BuiltinDirective::from_str(string) {
return Ok(Self::Builtin(builtin));
}
if let Some(config) = custom_directive_map.get(string) {
return Ok(Self::Custom(config.clone()));
}
Err(())
}
fn title(self, raw_directive: &str) -> String {
match self {
Directive::Builtin(_) => format_builtin_directive_title(raw_directive),
Directive::Custom(custom) => custom
.title
.clone()
.unwrap_or_else(|| uppercase_first(raw_directive)),
}
}
}
impl AdmonitionMeta {
pub fn from_info_string(
info_string: &str,
defaults: &AdmonitionDefaults,
overrides: &Overrides,
) -> Option<Result<Self, String>> {
InstanceConfig::from_info_string(info_string)
.map(|raw| raw.map(|raw| Self::resolve(raw, defaults)))
.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) -> Self {
fn resolve(raw: InstanceConfig, overrides: &Overrides) -> Self {
let InstanceConfig {
directive: raw_directive,
title,
@@ -35,15 +75,34 @@ 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(&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::from_str(&raw_directive), title) {
(Ok(directive), None) => (directive, format_directive_title(&raw_directive)),
(Err(_), None) => (Directive::Note, "Note".to_owned()),
(Ok(directive), Some(title)) => (directive, title),
(Err(_), Some(title)) => (Directive::Note, title),
let (directive, title) = match (directive, title) {
(Ok(directive), None) => (directive.to_string(), directive.title(&raw_directive)),
(Err(_), None) => (BuiltinDirective::Note.to_string(), "Note".to_owned()),
(Ok(directive), Some(title)) => (directive.to_string(), title),
(Err(_), Some(title)) => (BuiltinDirective::Note.to_string(), title),
};
let css_id = if let Some(verbatim) = id {
@@ -51,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()),
@@ -71,7 +131,7 @@ impl AdmonitionMeta {
/// Format the title of an admonition directive
///
/// We special case a few words to make them look nicer (e.g. "tldr" -> "TL;DR" and "faq" -> "FAQ").
fn format_directive_title(input: &str) -> String {
fn format_builtin_directive_title(input: &str) -> String {
match input {
"tldr" => "TL;DR".to_owned(),
"faq" => "FAQ".to_owned(),
@@ -92,19 +152,23 @@ 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;
#[test]
fn test_format_directive_title() {
assert_eq!(format_directive_title(""), "");
assert_eq!(format_directive_title("a"), "A");
assert_eq!(format_directive_title("tldr"), "TL;DR");
assert_eq!(format_directive_title("faq"), "FAQ");
assert_eq!(format_directive_title("note"), "Note");
assert_eq!(format_directive_title("abstract"), "Abstract");
fn test_format_builtin_directive_title() {
assert_eq!(format_builtin_directive_title(""), "");
assert_eq!(format_builtin_directive_title("a"), "A");
assert_eq!(format_builtin_directive_title("tldr"), "TL;DR");
assert_eq!(format_builtin_directive_title("faq"), "FAQ");
assert_eq!(format_builtin_directive_title("note"), "Note");
assert_eq!(format_builtin_directive_title("abstract"), "Abstract");
// Unicode
assert_eq!(format_directive_title("🦀"), "🦀");
assert_eq!(format_builtin_directive_title("🦀"), "🦀");
}
#[test]
@@ -118,10 +182,10 @@ mod test {
additional_classnames: Vec::new(),
collapsible: None,
},
&Default::default()
&Overrides::default(),
),
AdmonitionMeta {
directive: Directive::Note,
directive: "note".to_owned(),
title: "Note".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),
@@ -141,14 +205,17 @@ mod test {
additional_classnames: Vec::new(),
collapsible: None,
},
&AdmonitionDefaults {
title: Some("Important!!!".to_owned()),
css_id_prefix: Some("custom-prefix-".to_owned()),
collapsible: true,
},
&Overrides {
book: AdmonitionDefaults {
title: Some("Important!!!".to_owned()),
css_id_prefix: Some("custom-prefix-".to_owned()),
collapsible: true,
},
..Default::default()
}
),
AdmonitionMeta {
directive: Directive::Note,
directive: "note".to_owned(),
title: "Important!!!".to_owned(),
css_id: CssId::Prefix("custom-prefix-".to_owned()),
additional_classnames: Vec::new(),
@@ -168,14 +235,17 @@ 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,
},
&Overrides {
book: AdmonitionDefaults {
title: Some("Important!!!".to_owned()),
css_id_prefix: Some("ignored-custom-prefix-".to_owned()),
collapsible: true,
},
..Default::default()
}
),
AdmonitionMeta {
directive: Directive::Note,
directive: "note".to_owned(),
title: "Important!!!".to_owned(),
css_id: CssId::Verbatim("my-custom-id".to_owned()),
additional_classnames: Vec::new(),
@@ -183,4 +253,208 @@ mod test {
}
);
}
#[test]
fn test_admonition_info_from_raw_with_custom_directive() {
assert_eq!(
AdmonitionMeta::resolve(
InstanceConfig {
directive: "frog".to_owned(),
title: None,
id: None,
additional_classnames: Vec::new(),
collapsible: None,
},
&Overrides {
custom: [CustomDirective {
directive: "frog".to_owned(),
aliases: Vec::new(),
title: None,
collapsible: None,
}]
.into_iter()
.collect(),
..Default::default()
}
),
AdmonitionMeta {
directive: "frog".to_owned(),
title: "Frog".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),
collapsible: false,
}
);
}
#[test]
fn test_admonition_info_from_raw_with_custom_directive_and_title() {
assert_eq!(
AdmonitionMeta::resolve(
InstanceConfig {
directive: "frog".to_owned(),
title: None,
id: None,
additional_classnames: Vec::new(),
collapsible: None,
},
&Overrides {
custom: [CustomDirective {
directive: "frog".to_owned(),
aliases: Vec::new(),
title: Some("🏳️‍🌈".to_owned()),
collapsible: None,
}]
.into_iter()
.collect(),
..Default::default()
}
),
AdmonitionMeta {
directive: "frog".to_owned(),
title: "🏳️‍🌈".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),
collapsible: false,
}
);
}
#[test]
fn test_admonition_info_from_raw_with_custom_directive_alias() {
assert_eq!(
AdmonitionMeta::resolve(
InstanceConfig {
directive: "toad".to_owned(),
title: Some("Still a frog".to_owned()),
id: None,
additional_classnames: Vec::new(),
collapsible: None,
},
&Overrides {
custom: [CustomDirective {
directive: "frog".to_owned(),
aliases: vec!["newt".to_owned(), "toad".to_owned()],
title: Some("🏳️‍🌈".to_owned()),
collapsible: None,
}]
.into_iter()
.collect(),
..Default::default()
}
),
AdmonitionMeta {
directive: "frog".to_owned(),
title: "Still a frog".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),
collapsible: false,
}
);
}
#[test]
fn test_admonition_info_from_raw_with_collapsible_custom_directive() {
assert_eq!(
AdmonitionMeta::resolve(
InstanceConfig {
directive: "frog".to_owned(),
title: None,
id: None,
additional_classnames: Vec::new(),
collapsible: None,
},
&Overrides {
custom: [CustomDirective {
directive: "frog".to_owned(),
aliases: Vec::new(),
title: None,
collapsible: Some(true),
}]
.into_iter()
.collect(),
..Default::default()
}
),
AdmonitionMeta {
directive: "frog".to_owned(),
title: "Frog".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),
collapsible: true,
}
);
}
#[test]
fn test_admonition_info_from_raw_with_collapsible_builtin_directive() {
assert_eq!(
AdmonitionMeta::resolve(
InstanceConfig {
directive: "abstract".to_owned(),
title: None,
id: None,
additional_classnames: Vec::new(),
collapsible: None,
},
&Overrides {
book: AdmonitionDefaults {
title: None,
css_id_prefix: None,
collapsible: false,
},
builtin: HashMap::from([(
BuiltinDirective::Abstract,
BuiltinDirectiveConfig {
collapsible: Some(true),
}
)]),
..Default::default()
}
),
AdmonitionMeta {
directive: "abstract".to_owned(),
title: "Abstract".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),
collapsible: true,
}
);
}
#[test]
fn test_admonition_info_from_raw_with_non_collapsible_builtin_directive() {
assert_eq!(
AdmonitionMeta::resolve(
InstanceConfig {
directive: "abstract".to_owned(),
title: None,
id: None,
additional_classnames: Vec::new(),
collapsible: None,
},
&Overrides {
book: AdmonitionDefaults {
title: None,
css_id_prefix: None,
collapsible: true,
},
builtin: HashMap::from([(
BuiltinDirective::Abstract,
BuiltinDirectiveConfig {
collapsible: Some(false),
}
)]),
..Default::default()
}
),
AdmonitionMeta {
directive: "abstract".to_owned(),
title: "Abstract".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),
collapsible: false,
}
);
}
}

View File

@@ -0,0 +1,20 @@
:root {
--md-admonition-icon--admonish-note: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
}
:is(.admonition):is(.admonish-note) {
border-color: #448aff;
}
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(68, 138, 255, 0.1);
}
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #448aff;
mask-image: var(--md-admonition-icon--admonish-note);
-webkit-mask-image: var(--md-admonition-icon--admonish-note);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}

View File

@@ -1,9 +1,10 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
/// Book wide defaults that may be provided by the user.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct AdmonitionDefaults {
#[serde(default)]
pub(crate) title: Option<String>,
@@ -12,11 +13,20 @@ 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>,
}
#[derive(Debug, PartialEq)]
pub(crate) enum Directive {
/// First class supported directives by the crate.
///
/// These are guaranteed to have valid CSS/icons available.
///
/// Custom directives can also be added via the book.toml config.
#[derive(Debug, PartialEq, Clone, Copy, Eq, Deserialize, Serialize, Hash)]
#[serde(rename_all = "lowercase")]
pub(crate) enum BuiltinDirective {
Note,
Abstract,
Info,
@@ -31,7 +41,7 @@ pub(crate) enum Directive {
Quote,
}
impl FromStr for Directive {
impl FromStr for BuiltinDirective {
type Err = ();
fn from_str(string: &str) -> Result<Self, ()> {
@@ -53,6 +63,95 @@ impl FromStr for Directive {
}
}
impl fmt::Display for BuiltinDirective {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match self {
Self::Note => "note",
Self::Abstract => "abstract",
Self::Info => "info",
Self::Tip => "tip",
Self::Success => "success",
Self::Question => "question",
Self::Warning => "warning",
Self::Failure => "failure",
Self::Danger => "danger",
Self::Bug => "bug",
Self::Example => "example",
Self::Quote => "quote",
};
f.write_str(value)
}
}
/// The subset of information we care about during plugin runtime for custom directives.
///
/// This drops information only needed during CSS generation.
#[derive(Debug, Clone)]
pub(crate) struct CustomDirective {
pub directive: String,
pub aliases: Vec<String>,
pub title: Option<String>,
pub collapsible: Option<bool>,
}
impl From<(String, crate::book_config::CustomDirective)> for CustomDirective {
fn from((directive, config): (String, crate::book_config::CustomDirective)) -> Self {
let crate::book_config::CustomDirective {
aliases,
title,
collapsible,
..
} = config;
Self {
directive,
aliases,
title,
collapsible,
}
}
}
/// A map from the user given directive to underlying config.
///
/// The terminology is a bit mixed here - this map allows any input-directive,
/// and returns the output-directive config.
///
/// i.e. this is the step alias mapping happens at
#[derive(Debug, Clone, Default)]
pub(crate) struct CustomDirectiveMap {
inner: HashMap<String, CustomDirective>,
}
impl CustomDirectiveMap {
pub fn get(&self, key: &str) -> Option<&CustomDirective> {
self.inner.get(key)
}
}
impl FromIterator<CustomDirective> for CustomDirectiveMap {
fn from_iter<I: IntoIterator<Item = CustomDirective>>(iter: I) -> Self {
let mut inner = HashMap::default();
for config in iter.into_iter() {
inner
.entry(config.directive.clone())
.or_insert(config.clone());
for alias in config.aliases.iter() {
inner.entry(alias.clone()).or_insert(config.clone());
}
}
Self { inner }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct BuiltinDirectiveConfig {
/// Default collapsible value.
#[serde(default)]
pub collapsible: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RenderTextMode {
Strip,
@@ -70,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,
}

12
v2.md Normal file
View File

@@ -0,0 +1,12 @@
# v2 Notes
## Compatibility to drop
- v1 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