Compare commits

...

17 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
19 changed files with 1261 additions and 711 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
@@ -75,7 +80,7 @@ jobs:
rust:
- stable
- beta
- 1.66.0
- 1.74.0
experimental:
- false
include:
@@ -95,6 +100,8 @@ jobs:
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
@@ -111,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,19 @@
# 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

1079
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.15.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."
@@ -31,21 +31,22 @@ anyhow = "1.0.75"
# 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 }
env_logger = { version = "0.11", default-features = false, optional = true }
log = "0.4.20"
mdbook = "0.4.35"
once_cell = "1.18.0"
path-slash = "0.2.1"
pulldown-cmark = "0.9.3"
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]

View File

@@ -4,17 +4,16 @@ language = "en"
multilingual = false
src = "src"
title = "The mdbook-admonish book"
git-repository-url = "https://github.com/tommilligan/mdbook-admonish"
[preprocessor]
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "3.0.1" # do not edit: managed by `mdbook-admonish install`
assets_version = "3.0.2" # do not edit: managed by `mdbook-admonish install`
[[preprocessor.admonish.custom]]
directive = "expensive"
icon = "./money-bag.svg"
color = "#24ab38"
[preprocessor.admonish.directive.custom]
expensive = { icon = "./money-bag.svg", color = "#24ab38" }
[preprocessor.toc]
command = "mdbook-toc"

View File

@@ -75,28 +75,50 @@ Subfields:
- For the `html` renderer, the default value is `html`.
- For all other renderers, the default value is `preserve`.
### `custom`
### `directive`
Optional.
Additional type of block to support.
You must run `mdbook-admonish generate-custom` after updating these values, to generate the correct styles.
Settings relating to each type of block.
Add blocks using TOML's [Array of Tables](https://toml.io/en/v1.0.0#array-of-tables) notation:
#### `builtin`
Optional.
Override the settings of a builtin directive.
The subkey of `builtin` is the directive to override. This must be the first directive listed in the [Directives](#directives) section below, e.g. `warning` (not `caution` or other aliases).
```toml
[[preprocessor.admonish.custom]]
directive = "expensive"
[preprocessor.admonish.directive.builtin.warning]
collapsible = true
```
Subfields:
- `collapsible` (optional): The default boolean value of the collapsible property for this type of block.
#### `custom`
Optional.
Additional types of block to support. The subkey of `custom` is the new directive to support.
You must run `mdbook-admonish generate-custom` after updating these values, to generate the correct styles.
```toml
[preprocessor.admonish.directive.custom.expensive]
icon = "./money-bag.svg"
color = "#24ab38"
collapsible = true
aliases = ["money", "cash", "budget"]
```
Subfields:
- `directive`: The keyword to use this type of block.
- `icon`: A filepath relative to the book root to load an SVG icon from.
- `color`: An RGB hex encoded color to use for the icon.
- `collapsible` (optional): The default boolean value of the collapsible property for this type of block.
- `aliases` (optional): One or more alternative directives to use this block.
- `title` (optional): The default title for this type of block. If not specified, defaults to the directive in title case. To give each alias a custom title, add multiple custom blocks.

View File

@@ -39,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

@@ -160,7 +160,7 @@ mod install {
io::Write,
path::PathBuf,
};
use toml_edit::{self, Array, Document, Item, Table, Value};
use toml_edit::{self, Array, DocumentMut, Item, Table, Value};
const ADMONISH_CSS_FILES: &[(&str, &[u8])] = &[(
"mdbook-admonish.css",
@@ -196,7 +196,7 @@ mod install {
let toml = fs::read_to_string(&config)
.with_context(|| format!("can't read configuration file '{}'", config.display()))?;
let mut doc = toml
.parse::<Document>()
.parse::<DocumentMut>()
.context("configuration is not valid TOML")?;
if let Ok(preprocessor) = preprocessor(&mut doc) {
@@ -259,7 +259,7 @@ A beautifully styled message.
/// Return the `additional-css` field, initializing if required.
///
/// Return `Err` if the existing configuration is unknown.
fn additional_css(doc: &mut Document) -> Result<&mut Array, ()> {
fn additional_css(doc: &mut DocumentMut) -> Result<&mut Array, ()> {
let doc = doc.as_table_mut();
let empty_table = Item::Table(Table::default());
@@ -283,7 +283,7 @@ A beautifully styled message.
/// Return the preprocessor table for admonish, initializing if required.
///
/// Return `Err` if the existing configuration is unknown.
fn preprocessor(doc: &mut Document) -> Result<&mut Item, ()> {
fn preprocessor(doc: &mut DocumentMut) -> Result<&mut Item, ()> {
let doc = doc.as_table_mut();
let empty_table = Item::Table(Table::default());

View File

@@ -4,7 +4,7 @@ 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.
///
@@ -20,10 +20,43 @@ pub(crate) fn admonish_config_from_context(ctx: &PreprocessorContext) -> Result<
}
pub(crate) fn admonish_config_from_str(data: &str) -> Result<Config> {
toml::from_str(data).context("Invalid mdbook-admonish configuration in book.toml")
let readonly: ConfigReadonly =
toml::from_str(data).context("Invalid mdbook-admonish configuration in book.toml")?;
let config = readonly.into();
log::debug!("Loaded admonish config: {:?}", config);
Ok(config)
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
/// All valid input states including back-compatibility fields.
///
/// This struct deliberately does not implement Serialize as it never meant to
/// be written, only converted to Config.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
struct ConfigReadonly {
#[serde(default)]
pub on_failure: OnFailure,
#[serde(default)]
pub default: AdmonitionDefaults,
#[serde(default)]
pub renderer: HashMap<String, RendererConfig>,
#[serde(default)]
pub assets_version: Option<String>,
#[serde(default)]
pub custom: Vec<CustomDirectiveReadonly>,
#[serde(default)]
pub builtin: HashMap<BuiltinDirective, BuiltinDirectiveConfig>,
#[serde(default)]
pub directive: DirectiveConfig,
}
/// The canonical config format, without back-compatibility
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub(crate) struct Config {
#[serde(default)]
pub on_failure: OnFailure,
@@ -38,14 +71,50 @@ pub(crate) struct Config {
pub assets_version: Option<String>,
#[serde(default)]
pub custom: Vec<CustomDirective>,
pub directive: DirectiveConfig,
}
impl From<ConfigReadonly> for Config {
fn from(other: ConfigReadonly) -> Self {
let ConfigReadonly {
on_failure,
default,
renderer,
assets_version,
custom,
builtin,
mut directive,
} = other;
// Merge deprecated config fields into main config object
directive.custom.extend(
custom
.into_iter()
.map(|CustomDirectiveReadonly { directive, config }| (directive, config)),
);
directive.builtin.extend(builtin);
Self {
on_failure,
default,
renderer,
assets_version,
directive,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub(crate) struct DirectiveConfig {
#[serde(default)]
pub custom: HashMap<String, CustomDirective>,
#[serde(default)]
pub builtin: HashMap<BuiltinDirective, BuiltinDirectiveConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct CustomDirective {
/// The primary directive. Used for CSS classnames
pub directive: String,
/// Path to an SVG file, relative to the book root.
pub icon: PathBuf,
@@ -59,6 +128,20 @@ pub(crate) struct CustomDirective {
/// Title to use, human readable.
#[serde(default)]
pub title: Option<String>,
/// Default collapsible value.
#[serde(default)]
pub collapsible: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct CustomDirectiveReadonly {
/// The primary directive. Used for CSS classnames
pub directive: String,
/// Path to an SVG file, relative to the book root.
#[serde(flatten)]
config: CustomDirective,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
@@ -86,3 +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
"#
);
}

View File

@@ -12,8 +12,8 @@ use std::path::Path;
static RX_COLLAPSE_NEWLINES: Lazy<Regex> =
Lazy::new(|| Regex::new(r"[\r\n]+\s*").expect("invalid whitespace regex"));
/// Do some simple things to make the svg input probably a valid data url
/// Based on this gist: https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
// Do some simple things to make the svg input probably a valid data url
// Based on this gist: https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
fn svg_to_data_url(svg: &str) -> String {
const XMLNS: &str = r#"http://www.w3.org/2000/svg"#;
//
@@ -70,7 +70,7 @@ fn directive_css(name: &str, svg_data: &str, tint: HexColor) -> String {
#[doc(hidden)]
pub fn css_from_config(book_dir: &Path, config: &str) -> Result<String> {
let config = crate::book_config::admonish_config_from_str(config)?;
let custom_directives = config.custom;
let custom_directives = config.directive.custom;
if custom_directives.is_empty() {
return Err(anyhow!("No custom directives provided"));
@@ -78,10 +78,10 @@ pub fn css_from_config(book_dir: &Path, config: &str) -> Result<String> {
log::info!("Loaded {} custom directives", custom_directives.len());
let mut css = String::new();
for directive in custom_directives.iter() {
for (directive_name, directive) in custom_directives.iter() {
let svg = fs::read_to_string(book_dir.join(&directive.icon))
.with_context(|| format!("can't read icon file '{}'", directive.icon.display()))?;
css.push_str(&directive_css(&directive.directive, &svg, directive.color));
css.push_str(&directive_css(directive_name, &svg, directive.color));
}
Ok(css)
}

View File

@@ -4,14 +4,13 @@ use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag};
use crate::{
book_config::OnFailure,
parse::parse_admonition,
types::{AdmonitionDefaults, CustomDirectiveMap, RenderTextMode},
types::{Overrides, RenderTextMode},
};
pub(crate) fn preprocess(
content: &str,
on_failure: OnFailure,
admonition_defaults: &AdmonitionDefaults,
custom_directives: &CustomDirectiveMap,
overrides: &Overrides,
render_text_mode: RenderTextMode,
) -> MdbookResult<String> {
let mut id_counter = Default::default();
@@ -33,8 +32,7 @@ pub(crate) fn preprocess(
let admonition = match parse_admonition(
info_string.as_ref(),
admonition_defaults,
custom_directives,
overrides,
span_content,
on_failure,
indent,
@@ -92,9 +90,12 @@ fn indent_of(content: &str, position: usize, max: usize) -> usize {
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use crate::types::AdmonitionDefaults;
use super::*;
#[test]
fn indent_of_samples() {
for (content, position, max, expected) in [
@@ -137,8 +138,7 @@ mod test {
preprocess(
content,
OnFailure::Continue,
&AdmonitionDefaults::default(),
&CustomDirectiveMap::default(),
&Overrides::default(),
RenderTextMode::Html,
)
.unwrap()
@@ -596,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="
@@ -631,8 +633,7 @@ Bonus content!
preprocess(
content,
OnFailure::Bail,
&AdmonitionDefaults::default(),
&CustomDirectiveMap::default(),
&Overrides::default(),
RenderTextMode::Html
)
.unwrap_err()
@@ -659,8 +660,7 @@ x = 20;
preprocess(
content,
OnFailure::Bail,
&AdmonitionDefaults::default(),
&CustomDirectiveMap::default(),
&Overrides::default(),
RenderTextMode::Strip
)
.unwrap(),
@@ -734,12 +734,14 @@ Text
let preprocess_result = preprocess(
content,
OnFailure::Continue,
&AdmonitionDefaults {
title: Some("Admonish".to_owned()),
css_id_prefix: None,
collapsible: false,
&Overrides {
book: AdmonitionDefaults {
title: Some("Admonish".to_owned()),
css_id_prefix: None,
collapsible: false,
},
..Default::default()
},
&CustomDirectiveMap::default(),
RenderTextMode::Html,
)
.unwrap();
@@ -770,12 +772,14 @@ Text
let preprocess_result = preprocess(
content,
OnFailure::Continue,
&AdmonitionDefaults {
title: Some("Admonish".to_owned()),
css_id_prefix: None,
collapsible: false,
&Overrides {
book: AdmonitionDefaults {
title: Some("Admonish".to_owned()),
css_id_prefix: None,
collapsible: false,
},
..Default::default()
},
&CustomDirectiveMap::default(),
RenderTextMode::Html,
)
.unwrap();
@@ -926,12 +930,14 @@ Text
let preprocess_result = preprocess(
content,
OnFailure::Continue,
&AdmonitionDefaults {
title: Some("Info".to_owned()),
css_id_prefix: Some("".to_owned()),
collapsible: false,
&Overrides {
book: AdmonitionDefaults {
title: Some("Info".to_owned()),
css_id_prefix: Some("".to_owned()),
collapsible: false,
},
..Default::default()
},
&CustomDirectiveMap::default(),
RenderTextMode::Html,
)
.unwrap();
@@ -968,12 +974,14 @@ Text
let preprocess_result = preprocess(
content,
OnFailure::Continue,
&AdmonitionDefaults {
title: Some("Info".to_owned()),
css_id_prefix: Some("prefix-".to_owned()),
collapsible: false,
&Overrides {
book: AdmonitionDefaults {
title: Some("Info".to_owned()),
css_id_prefix: Some("prefix-".to_owned()),
collapsible: false,
},
..Default::default()
},
&CustomDirectiveMap::default(),
RenderTextMode::Html,
)
.unwrap();
@@ -1010,12 +1018,14 @@ Text
let preprocess_result = preprocess(
content,
OnFailure::Continue,
&AdmonitionDefaults {
title: Some("Info".to_owned()),
css_id_prefix: Some("ignored-prefix-".to_owned()),
collapsible: false,
&Overrides {
book: AdmonitionDefaults {
title: Some("Info".to_owned()),
css_id_prefix: Some("ignored-prefix-".to_owned()),
collapsible: false,
},
..Default::default()
},
&CustomDirectiveMap::default(),
RenderTextMode::Html,
)
.unwrap();

View File

@@ -5,7 +5,7 @@ use crate::{
book_config::OnFailure,
render::Admonition,
resolve::AdmonitionMeta,
types::{AdmonitionDefaults, BuiltinDirective, CssId, CustomDirectiveMap},
types::{BuiltinDirective, CssId, Overrides},
};
/// Given the content in the span of the code block, and the info string,
@@ -19,8 +19,7 @@ use crate::{
/// If the code block is not an admonition, return `None`.
pub(crate) fn parse_admonition<'a>(
info_string: &'a str,
admonition_defaults: &'a AdmonitionDefaults,
custom_directives: &'a CustomDirectiveMap,
overrides: &'a Overrides,
content: &'a str,
on_failure: OnFailure,
indent: usize,
@@ -28,8 +27,7 @@ pub(crate) fn parse_admonition<'a>(
// We need to know fence details anyway for error messages
let extracted = extract_admonish_body(content);
let info =
AdmonitionMeta::from_info_string(info_string, admonition_defaults, custom_directives)?;
let info = AdmonitionMeta::from_info_string(info_string, overrides)?;
let info = match info {
Ok(info) => info,
Err(message) => {

View File

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

View File

@@ -1,7 +1,5 @@
use crate::config::InstanceConfig;
use crate::types::{
AdmonitionDefaults, BuiltinDirective, CssId, CustomDirective, CustomDirectiveMap,
};
use crate::types::{BuiltinDirective, CssId, CustomDirective, CustomDirectiveMap, Overrides};
use std::fmt;
use std::str::FromStr;
@@ -59,20 +57,15 @@ impl Directive {
impl AdmonitionMeta {
pub fn from_info_string(
info_string: &str,
defaults: &AdmonitionDefaults,
custom_directives: &CustomDirectiveMap,
overrides: &Overrides,
) -> Option<Result<Self, String>> {
InstanceConfig::from_info_string(info_string)
.map(|raw| raw.map(|raw| Self::resolve(raw, defaults, custom_directives)))
.map(|raw| raw.map(|raw| Self::resolve(raw, overrides)))
}
/// Combine the per-admonition configuration with global defaults (and
/// other logic) to resolve the values needed for rendering.
fn resolve(
raw: InstanceConfig,
defaults: &AdmonitionDefaults,
custom_directives: &CustomDirectiveMap,
) -> Self {
fn resolve(raw: InstanceConfig, overrides: &Overrides) -> Self {
let InstanceConfig {
directive: raw_directive,
title,
@@ -82,10 +75,27 @@ impl AdmonitionMeta {
} = raw;
// Use values from block, else load default value
let title = title.or_else(|| defaults.title.clone());
let collapsible = collapsible.unwrap_or(defaults.collapsible);
let title = title.or_else(|| overrides.book.title.clone());
let directive = Directive::from_str(custom_directives, &raw_directive);
let directive = Directive::from_str(&overrides.custom, &raw_directive);
let collapsible = match directive {
// If the directive is a builin one, use collapsible from block, else use default
// value of the builtin directive, else use global default value
Ok(Directive::Builtin(directive)) => collapsible.unwrap_or(
overrides
.builtin
.get(&directive)
.and_then(|config| config.collapsible)
.unwrap_or(overrides.book.collapsible),
),
// If the directive is a custom one, use collapsible from block, else use default
// value of the custom directive, else use global default value
Ok(Directive::Custom(ref custom_dir)) => {
collapsible.unwrap_or(custom_dir.collapsible.unwrap_or(overrides.book.collapsible))
}
Err(_) => collapsible.unwrap_or(overrides.book.collapsible),
};
// Load the directive (and title, if one still not given)
let (directive, title) = match (directive, title) {
@@ -100,7 +110,8 @@ impl AdmonitionMeta {
} else {
const DEFAULT_CSS_ID_PREFIX: &str = "admonition-";
CssId::Prefix(
defaults
overrides
.book
.css_id_prefix
.clone()
.unwrap_or_else(|| DEFAULT_CSS_ID_PREFIX.to_owned()),
@@ -141,6 +152,10 @@ fn uppercase_first(input: &str) -> String {
#[cfg(test)]
mod test {
use std::collections::HashMap;
use crate::types::{AdmonitionDefaults, BuiltinDirectiveConfig};
use super::*;
use pretty_assertions::assert_eq;
@@ -167,8 +182,7 @@ mod test {
additional_classnames: Vec::new(),
collapsible: None,
},
&Default::default(),
&CustomDirectiveMap::default(),
&Overrides::default(),
),
AdmonitionMeta {
directive: "note".to_owned(),
@@ -191,12 +205,14 @@ mod test {
additional_classnames: Vec::new(),
collapsible: None,
},
&AdmonitionDefaults {
title: Some("Important!!!".to_owned()),
css_id_prefix: Some("custom-prefix-".to_owned()),
collapsible: true,
},
&CustomDirectiveMap::default(),
&Overrides {
book: AdmonitionDefaults {
title: Some("Important!!!".to_owned()),
css_id_prefix: Some("custom-prefix-".to_owned()),
collapsible: true,
},
..Default::default()
}
),
AdmonitionMeta {
directive: "note".to_owned(),
@@ -219,12 +235,14 @@ mod test {
additional_classnames: Vec::new(),
collapsible: None,
},
&AdmonitionDefaults {
title: Some("Important!!!".to_owned()),
css_id_prefix: Some("ignored-custom-prefix-".to_owned()),
collapsible: true,
},
&CustomDirectiveMap::default(),
&Overrides {
book: AdmonitionDefaults {
title: Some("Important!!!".to_owned()),
css_id_prefix: Some("ignored-custom-prefix-".to_owned()),
collapsible: true,
},
..Default::default()
}
),
AdmonitionMeta {
directive: "note".to_owned(),
@@ -247,12 +265,17 @@ mod test {
additional_classnames: Vec::new(),
collapsible: None,
},
&AdmonitionDefaults::default(),
&CustomDirectiveMap::from_configs(vec![CustomDirective {
directive: "frog".to_owned(),
aliases: Vec::new(),
title: None,
}]),
&Overrides {
custom: [CustomDirective {
directive: "frog".to_owned(),
aliases: Vec::new(),
title: None,
collapsible: None,
}]
.into_iter()
.collect(),
..Default::default()
}
),
AdmonitionMeta {
directive: "frog".to_owned(),
@@ -275,12 +298,17 @@ mod test {
additional_classnames: Vec::new(),
collapsible: None,
},
&AdmonitionDefaults::default(),
&CustomDirectiveMap::from_configs(vec![CustomDirective {
directive: "frog".to_owned(),
aliases: Vec::new(),
title: Some("🏳️‍🌈".to_owned()),
}]),
&Overrides {
custom: [CustomDirective {
directive: "frog".to_owned(),
aliases: Vec::new(),
title: Some("🏳️‍🌈".to_owned()),
collapsible: None,
}]
.into_iter()
.collect(),
..Default::default()
}
),
AdmonitionMeta {
directive: "frog".to_owned(),
@@ -303,12 +331,17 @@ mod test {
additional_classnames: Vec::new(),
collapsible: None,
},
&AdmonitionDefaults::default(),
&CustomDirectiveMap::from_configs(vec![CustomDirective {
directive: "frog".to_owned(),
aliases: vec!["newt".to_owned(), "toad".to_owned()],
title: Some("🏳️‍🌈".to_owned()),
}]),
&Overrides {
custom: [CustomDirective {
directive: "frog".to_owned(),
aliases: vec!["newt".to_owned(), "toad".to_owned()],
title: Some("🏳️‍🌈".to_owned()),
collapsible: None,
}]
.into_iter()
.collect(),
..Default::default()
}
),
AdmonitionMeta {
directive: "frog".to_owned(),
@@ -319,4 +352,109 @@ mod test {
}
);
}
#[test]
fn test_admonition_info_from_raw_with_collapsible_custom_directive() {
assert_eq!(
AdmonitionMeta::resolve(
InstanceConfig {
directive: "frog".to_owned(),
title: None,
id: None,
additional_classnames: Vec::new(),
collapsible: None,
},
&Overrides {
custom: [CustomDirective {
directive: "frog".to_owned(),
aliases: Vec::new(),
title: None,
collapsible: Some(true),
}]
.into_iter()
.collect(),
..Default::default()
}
),
AdmonitionMeta {
directive: "frog".to_owned(),
title: "Frog".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),
collapsible: true,
}
);
}
#[test]
fn test_admonition_info_from_raw_with_collapsible_builtin_directive() {
assert_eq!(
AdmonitionMeta::resolve(
InstanceConfig {
directive: "abstract".to_owned(),
title: None,
id: None,
additional_classnames: Vec::new(),
collapsible: None,
},
&Overrides {
book: AdmonitionDefaults {
title: None,
css_id_prefix: None,
collapsible: false,
},
builtin: HashMap::from([(
BuiltinDirective::Abstract,
BuiltinDirectiveConfig {
collapsible: Some(true),
}
)]),
..Default::default()
}
),
AdmonitionMeta {
directive: "abstract".to_owned(),
title: "Abstract".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),
collapsible: true,
}
);
}
#[test]
fn test_admonition_info_from_raw_with_non_collapsible_builtin_directive() {
assert_eq!(
AdmonitionMeta::resolve(
InstanceConfig {
directive: "abstract".to_owned(),
title: None,
id: None,
additional_classnames: Vec::new(),
collapsible: None,
},
&Overrides {
book: AdmonitionDefaults {
title: None,
css_id_prefix: None,
collapsible: true,
},
builtin: HashMap::from([(
BuiltinDirective::Abstract,
BuiltinDirectiveConfig {
collapsible: Some(false),
}
)]),
..Default::default()
}
),
AdmonitionMeta {
directive: "abstract".to_owned(),
title: "Abstract".to_owned(),
css_id: CssId::Prefix("admonition-".to_owned()),
additional_classnames: Vec::new(),
collapsible: false,
}
);
}
}

View File

@@ -5,7 +5,6 @@ use std::str::FromStr;
/// Book wide defaults that may be provided by the user.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct AdmonitionDefaults {
#[serde(default)]
pub(crate) title: Option<String>,
@@ -14,6 +13,9 @@ pub(crate) struct AdmonitionDefaults {
pub(crate) collapsible: bool,
#[serde(default)]
// For backwards compatibility, we support this field with kebab-case style
// naming, even though this was introduced in error.
#[serde(alias = "css-id-prefix")]
pub(crate) css_id_prefix: Option<String>,
}
@@ -22,7 +24,8 @@ pub(crate) struct AdmonitionDefaults {
/// These are guaranteed to have valid CSS/icons available.
///
/// Custom directives can also be added via the book.toml config.
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone, Copy, Eq, Deserialize, Serialize, Hash)]
#[serde(rename_all = "lowercase")]
pub(crate) enum BuiltinDirective {
Note,
Abstract,
@@ -83,25 +86,27 @@ impl fmt::Display for BuiltinDirective {
/// The subset of information we care about during plugin runtime for custom directives.
///
/// This drops information only needed during CSS generation.
#[derive(Clone)]
#[derive(Debug, Clone)]
pub(crate) struct CustomDirective {
pub directive: String,
pub aliases: Vec<String>,
pub title: Option<String>,
pub collapsible: Option<bool>,
}
impl From<crate::book_config::CustomDirective> for CustomDirective {
fn from(other: crate::book_config::CustomDirective) -> Self {
impl From<(String, crate::book_config::CustomDirective)> for CustomDirective {
fn from((directive, config): (String, crate::book_config::CustomDirective)) -> Self {
let crate::book_config::CustomDirective {
directive,
aliases,
title,
collapsible,
..
} = other;
} = config;
Self {
directive,
aliases,
title,
collapsible,
}
}
}
@@ -112,7 +117,7 @@ impl From<crate::book_config::CustomDirective> for CustomDirective {
/// and returns the output-directive config.
///
/// i.e. this is the step alias mapping happens at
#[derive(Default)]
#[derive(Debug, Clone, Default)]
pub(crate) struct CustomDirectiveMap {
inner: HashMap<String, CustomDirective>,
}
@@ -121,19 +126,18 @@ impl CustomDirectiveMap {
pub fn get(&self, key: &str) -> Option<&CustomDirective> {
self.inner.get(key)
}
}
pub fn from_configs<T>(configs: T) -> Self
where
T: IntoIterator<Item = CustomDirective>,
{
impl FromIterator<CustomDirective> for CustomDirectiveMap {
fn from_iter<I: IntoIterator<Item = CustomDirective>>(iter: I) -> Self {
let mut inner = HashMap::default();
for directive in configs.into_iter() {
for config in iter.into_iter() {
inner
.entry(directive.directive.clone())
.or_insert(directive.clone());
.entry(config.directive.clone())
.or_insert(config.clone());
for alias in directive.aliases.iter() {
inner.entry(alias.clone()).or_insert(directive.clone());
for alias in config.aliases.iter() {
inner.entry(alias.clone()).or_insert(config.clone());
}
}
@@ -141,6 +145,13 @@ impl CustomDirectiveMap {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct BuiltinDirectiveConfig {
/// Default collapsible value.
#[serde(default)]
pub collapsible: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RenderTextMode {
Strip,
@@ -158,3 +169,10 @@ pub(crate) enum CssId {
/// will generate the rest of the id based on the title
Prefix(String),
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Overrides {
pub book: AdmonitionDefaults,
pub builtin: HashMap<BuiltinDirective, BuiltinDirectiveConfig>,
pub custom: CustomDirectiveMap,
}

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