From 5d5b73ded600c735b561ec83ef63a447327aaa1d Mon Sep 17 00:00:00 2001 From: Yannick Seurin Date: Sun, 28 Apr 2024 14:31:23 +0200 Subject: [PATCH] allow to set collapsible default value for each directive --- book/book.toml | 8 +- book/src/reference.md | 36 +++++-- src/book_config.rs | 180 +++++++++++++++++++++++++++++--- src/custom.rs | 6 +- src/markdown.rs | 82 ++++++++------- src/parse.rs | 8 +- src/preprocessor.rs | 31 +++--- src/resolve.rs | 232 +++++++++++++++++++++++++++++++++--------- src/types.rs | 48 ++++++--- 9 files changed, 483 insertions(+), 148 deletions(-) diff --git a/book/book.toml b/book/book.toml index 75c38b3..7076031 100644 --- a/book/book.toml +++ b/book/book.toml @@ -10,12 +10,10 @@ git-repository-url = "https://github.com/tommilligan/mdbook-admonish" [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" diff --git a/book/src/reference.md b/book/src/reference.md index 758a25e..55016da 100644 --- a/book/src/reference.md +++ b/book/src/reference.md @@ -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. + +#### `builtin` + +Optional. + +Override the settings of a builtin directive. -Add blocks using TOML's [Array of Tables](https://toml.io/en/v1.0.0#array-of-tables) notation: +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. diff --git a/src/book_config.rs b/src/book_config.rs index 3080b1e..39507a3 100644 --- a/src/book_config.rs +++ b/src/book_config.rs @@ -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,9 +20,42 @@ pub(crate) fn admonish_config_from_context(ctx: &PreprocessorContext) -> Result< } pub(crate) fn admonish_config_from_str(data: &str) -> Result { - 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) } +/// 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, + + #[serde(default)] + pub assets_version: Option, + + #[serde(default)] + pub custom: Vec, + + #[serde(default)] + pub builtin: HashMap, + + #[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)] @@ -38,14 +71,50 @@ pub(crate) struct Config { pub assets_version: Option, #[serde(default)] - pub custom: Vec, + pub directive: DirectiveConfig, +} + +impl From 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, + + #[serde(default)] + pub builtin: HashMap, } #[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, + + /// Default collapsible value. + #[serde(default)] + pub collapsible: Option, +} + +#[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)] @@ -92,6 +175,8 @@ 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("")?; @@ -120,6 +205,57 @@ mod test { 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 { @@ -129,13 +265,24 @@ mod test { title: Some("".to_owned()), }, assets_version: Some("1.1.1".to_owned()), - custom: vec![CustomDirective { - directive: "test-directive".to_owned(), - 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()), - }], + 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(), @@ -156,12 +303,15 @@ css_id_prefix = "flam-" [renderer.test-mode] render_mode = "strip" -[[custom]] -directive = "test-directive" +[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)?; diff --git a/src/custom.rs b/src/custom.rs index 8363dbf..caab508 100644 --- a/src/custom.rs +++ b/src/custom.rs @@ -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 { 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 { 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) } diff --git a/src/markdown.rs b/src/markdown.rs index f5fff41..6b901b8 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -4,14 +4,13 @@ use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag}; use crate::{ book_config::OnFailure, parse::parse_admonition, - types::{AdmonitionDefaults, CustomDirectiveMap, RenderTextMode}, + types::{Overrides, RenderTextMode}, }; pub(crate) fn preprocess( content: &str, on_failure: OnFailure, - admonition_defaults: &AdmonitionDefaults, - custom_directives: &CustomDirectiveMap, + overrides: &Overrides, render_text_mode: RenderTextMode, ) -> MdbookResult { 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() @@ -631,8 +631,7 @@ Bonus content! preprocess( content, OnFailure::Bail, - &AdmonitionDefaults::default(), - &CustomDirectiveMap::default(), + &Overrides::default(), RenderTextMode::Html ) .unwrap_err() @@ -659,8 +658,7 @@ x = 20; preprocess( content, OnFailure::Bail, - &AdmonitionDefaults::default(), - &CustomDirectiveMap::default(), + &Overrides::default(), RenderTextMode::Strip ) .unwrap(), @@ -734,12 +732,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 +770,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 +928,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 +972,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 +1016,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(); diff --git a/src/parse.rs b/src/parse.rs index 00f45d2..5e52828 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -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) => { diff --git a/src/preprocessor.rs b/src/preprocessor.rs index 30f53d3..b6fdb45 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -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; + }, + ), ); } }); diff --git a/src/resolve.rs b/src/resolve.rs index 91b6513..90b4994 100644 --- a/src/resolve.rs +++ b/src/resolve.rs @@ -1,7 +1,5 @@ use crate::config::InstanceConfig; -use crate::types::{ - AdmonitionDefaults, BuiltinDirective, CssId, CustomDirective, CustomDirectiveMap, -}; +use crate::types::{BuiltinDirective, CssId, CustomDirective, CustomDirectiveMap, Overrides}; use std::fmt; use std::str::FromStr; @@ -59,20 +57,15 @@ impl Directive { impl AdmonitionMeta { pub fn from_info_string( info_string: &str, - defaults: &AdmonitionDefaults, - custom_directives: &CustomDirectiveMap, + overrides: &Overrides, ) -> Option> { 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, + } + ); + } } diff --git a/src/types.rs b/src/types.rs index fdf7dc2..d32abc9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -24,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, @@ -85,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, pub title: Option, + pub collapsible: Option, } -impl From 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, } } } @@ -114,7 +117,7 @@ impl From 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, } @@ -123,19 +126,18 @@ impl CustomDirectiveMap { pub fn get(&self, key: &str) -> Option<&CustomDirective> { self.inner.get(key) } +} - pub fn from_configs(configs: T) -> Self - where - T: IntoIterator, - { +impl FromIterator for CustomDirectiveMap { + fn from_iter>(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()); } } @@ -143,6 +145,13 @@ impl CustomDirectiveMap { } } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub(crate) struct BuiltinDirectiveConfig { + /// Default collapsible value. + #[serde(default)] + pub collapsible: Option, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum RenderTextMode { Strip, @@ -160,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, + pub custom: CustomDirectiveMap, +}