diff options
author | Shaun Hamilton <shauhami020@gmail.com> | 2023-04-15 14:31:03 +0000 |
---|---|---|
committer | Tom Milligan <tom.milligan@uipath.com> | 2023-04-20 15:43:56 +0100 |
commit | d2698387651431f9ef8a9d5b2d4a84c91de510c5 (patch) | |
tree | f98eb72f467a91a6cbcd3c92e4ad50e3bb532a08 | |
parent | 082359e56262785fc145d32806b567c31f27fc02 (diff) |
feat: add book-wide default values
-rw-r--r-- | CHANGELOG.md | 6 | ||||
-rw-r--r-- | src/config/mod.rs | 87 | ||||
-rw-r--r-- | src/config/v1.rs | 12 | ||||
-rw-r--r-- | src/config/v2.rs | 22 | ||||
-rw-r--r-- | src/lib.rs | 205 | ||||
-rw-r--r-- | src/resolve.rs | 92 | ||||
-rw-r--r-- | src/types.rs | 11 |
7 files changed, 320 insertions, 115 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index ac8ff91..3f1222e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- User can set book-wide default for title and collapsible properties ([#84](https://github.com/tommilligan/mdbook-admonish/pull/84)), thanks to [@ShaunSHamilton](https://github.com/ShaunSHamilton) + ### Changed - MSRV (minimum supported rust version) is now 1.64.0 for clap v4 ([#79](https://github.com/tommilligan/mdbook-admonish/pull/79)) @@ -27,7 +31,7 @@ ### Added - Support key/value configuration ([#24](https://github.com/tommilligan/mdbook-admonish/pull/24), thanks [@gggto](https://github.com/gggto) and [@schungx](https://github.com/schungx) for design input) -- Support collapsiable admonition bodies ([#26](https://github.com/tommilligan/mdbook-admonish/pull/26), thanks [@gggto](https://github.com/gggto) for the suggestion and implementation!) +- Support collapsible admonition bodies ([#26](https://github.com/tommilligan/mdbook-admonish/pull/26), thanks [@gggto](https://github.com/gggto) for the suggestion and implementation!) - Make anchor links hoverable ([#27](https://github.com/tommilligan/mdbook-admonish/pull/27)) - Better handling for misconfigured admonitions ([#25](https://github.com/tommilligan/mdbook-admonish/pull/25)) - Nicer in-book error messages diff --git a/src/config/mod.rs b/src/config/mod.rs index b6dbc40..131ed22 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,15 +1,16 @@ -use crate::types::Directive; -use std::str::FromStr; - mod v1; mod v2; +/// Configuration as described by the instance of an admonition in markdown. +/// +/// This structure represents the configuration the user must provide in each +/// instance. #[derive(Debug, PartialEq)] pub(crate) struct AdmonitionInfoRaw { - directive: String, - title: Option<String>, - additional_classnames: Vec<String>, - collapsible: bool, + pub(crate) directive: String, + pub(crate) title: Option<String>, + pub(crate) additional_classnames: Vec<String>, + pub(crate) collapsible: Option<bool>, } /// Extract the remaining info string, if this is an admonition block. @@ -52,56 +53,6 @@ impl AdmonitionInfoRaw { } } -#[derive(Debug, PartialEq)] -pub(crate) struct AdmonitionInfo { - pub directive: Directive, - pub title: Option<String>, - pub additional_classnames: Vec<String>, - pub collapsible: bool, -} - -impl AdmonitionInfo { - pub fn from_info_string(info_string: &str) -> Option<Result<Self, String>> { - AdmonitionInfoRaw::from_info_string(info_string).map(|result| result.map(Into::into)) - } -} - -impl From<AdmonitionInfoRaw> for AdmonitionInfo { - fn from(other: AdmonitionInfoRaw) -> Self { - let AdmonitionInfoRaw { - directive: raw_directive, - title, - additional_classnames, - collapsible, - } = other; - let (directive, title) = match (Directive::from_str(&raw_directive), title) { - (Ok(directive), None) => (directive, ucfirst(&raw_directive)), - (Err(_), None) => (Directive::Note, "Note".to_owned()), - (Ok(directive), Some(title)) => (directive, title), - (Err(_), Some(title)) => (Directive::Note, title), - }; - // If the user explicitly gave no title, then disable the title bar - let title = if title.is_empty() { None } else { Some(title) }; - Self { - directive, - title, - additional_classnames, - collapsible, - } - } -} - -/// Make the first letter of `input` upppercase. -/// -/// source: https://stackoverflow.com/a/38406885 -fn ucfirst(input: &str) -> String { - let mut chars = input.chars(); - match chars.next() { - None => String::new(), - Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(), - } -} - #[cfg(test)] mod test { use super::*; @@ -121,7 +72,7 @@ mod test { directive: "note".to_owned(), title: None, additional_classnames: vec!["additional-classname".to_owned()], - collapsible: false, + collapsible: None, } ); // v2 syntax is supported @@ -133,25 +84,7 @@ mod test { directive: "question".to_owned(), title: Some("Custom Title".to_owned()), additional_classnames: Vec::new(), - collapsible: false, - } - ); - } - - #[test] - fn test_admonition_info_from_raw() { - assert_eq!( - AdmonitionInfo::from(AdmonitionInfoRaw { - directive: " ".to_owned(), - title: None, - additional_classnames: Vec::new(), - collapsible: false, - }), - AdmonitionInfo { - directive: Directive::Note, - title: Some("Note".to_owned()), - additional_classnames: Vec::new(), - collapsible: false, + collapsible: None, } ); } diff --git a/src/config/v1.rs b/src/config/v1.rs index a38dd0d..52641c7 100644 --- a/src/config/v1.rs +++ b/src/config/v1.rs @@ -53,7 +53,7 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<AdmonitionInfoRa directive: directive.to_owned(), title, additional_classnames, - collapsible: false, + collapsible: None, }) } @@ -70,7 +70,7 @@ mod test { directive: "".to_owned(), title: None, additional_classnames: Vec::new(), - collapsible: false, + collapsible: None, } ); assert_eq!( @@ -79,7 +79,7 @@ mod test { directive: "".to_owned(), title: None, additional_classnames: Vec::new(), - collapsible: false, + collapsible: None, } ); assert_eq!( @@ -88,7 +88,7 @@ mod test { directive: "unknown".to_owned(), title: None, additional_classnames: Vec::new(), - collapsible: false, + collapsible: None, } ); assert_eq!( @@ -97,7 +97,7 @@ mod test { directive: "note".to_owned(), title: None, additional_classnames: Vec::new(), - collapsible: false, + collapsible: None, } ); assert_eq!( @@ -106,7 +106,7 @@ mod test { directive: "note".to_owned(), title: None, additional_classnames: vec!["additional-classname".to_owned()], - collapsible: false, + collapsible: None, } ); } diff --git a/src/config/v2.rs b/src/config/v2.rs index 503ae9b..c5550c2 100644 --- a/src/config/v2.rs +++ b/src/config/v2.rs @@ -12,7 +12,7 @@ struct AdmonitionInfoConfig { #[serde(default)] class: Option<String>, #[serde(default)] - collapsible: bool, + collapsible: Option<bool>, } /// Transform our config string into valid toml @@ -106,7 +106,7 @@ mod test { directive: "".to_owned(), title: None, additional_classnames: Vec::new(), - collapsible: false, + collapsible: None, } ); assert_eq!( @@ -115,17 +115,19 @@ mod test { directive: "".to_owned(), title: None, additional_classnames: Vec::new(), - collapsible: false, + collapsible: None, } ); assert_eq!( - from_config_string(r#"type="note" class="additional classname" title="Никита""#) - .unwrap(), + from_config_string( + r#"type="note" class="additional classname" title="Никита" collapsible=true"# + ) + .unwrap(), AdmonitionInfoRaw { directive: "note".to_owned(), title: Some("Никита".to_owned()), additional_classnames: vec!["additional".to_owned(), "classname".to_owned()], - collapsible: false, + collapsible: Some(true), } ); // Specifying unknown keys is okay, as long as they're valid @@ -135,7 +137,7 @@ mod test { directive: "".to_owned(), title: None, additional_classnames: Vec::new(), - collapsible: false, + collapsible: None, } ); // Just directive is fine @@ -145,17 +147,17 @@ mod test { directive: "info".to_owned(), title: None, additional_classnames: Vec::new(), - collapsible: false, + collapsible: None, } ); // Directive plus toml config assert_eq!( - from_config_string(r#"info title="Information""#).unwrap(), + from_config_string(r#"info title="Information" collapsible=false"#).unwrap(), AdmonitionInfoRaw { directive: "info".to_owned(), title: Some("Information".to_owned()), additional_classnames: Vec::new(), - collapsible: false, + collapsible: Some(false), } ); // Directive after toml config is an error @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use mdbook::{ book::{Book, BookItem}, errors::Result as MdbookResult, @@ -9,9 +9,13 @@ use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag}; use std::{borrow::Cow, str::FromStr}; mod config; +mod resolve; mod types; -use crate::{config::AdmonitionInfo, types::Directive}; +use crate::{ + resolve::AdmonitionInfo, + types::{AdmonitionDefaults, Directive}, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum OnFailure { @@ -66,7 +70,7 @@ impl Preprocessor for Admonish { } if let BookItem::Chapter(ref mut chapter) = *item { - res = Some(preprocess(&chapter.content, on_failure).map(|md| { + res = Some(preprocess(&chapter.content, ctx, on_failure).map(|md| { chapter.content = md; })); } @@ -140,7 +144,7 @@ impl Directive { #[derive(Debug, PartialEq)] struct Admonition<'a> { directive: Directive, - title: Option<String>, + title: String, content: Cow<'a, str>, additional_classnames: Vec<String>, collapsible: bool, @@ -170,20 +174,19 @@ impl<'a> Admonition<'a> { let title_block = if self.collapsible { "summary" } else { "div" }; - let title_html = title - .as_ref() - .map(|title| { - Cow::Owned(format!( - r##"<{title_block} class="admonition-title"> + let title_html = if !title.is_empty() { + Cow::Owned(format!( + r##"<{title_block} class="admonition-title"> {title} <a class="admonition-anchor-link" href="#{ANCHOR_ID_PREFIX}-{anchor_id}"></a> </{title_block}> "## - )) - }) - .unwrap_or(Cow::Borrowed("")); + )) + } else { + Cow::Borrowed("") + }; if !self.additional_classnames.is_empty() { let mut buffer = additional_class.into_owned(); @@ -248,10 +251,11 @@ fn extract_admonish_body(content: &str) -> &str { /// If the code block is not an admonition, return `None`. fn parse_admonition<'a>( info_string: &'a str, + admonition_defaults: &'a AdmonitionDefaults, content: &'a str, on_failure: OnFailure, ) -> Option<MdbookResult<Admonition<'a>>> { - let info = AdmonitionInfo::from_info_string(info_string)?; + let info = AdmonitionInfo::from_info_string(info_string, admonition_defaults)?; let info = match info { Ok(info) => info, // FIXME return error messages to break build if configured @@ -260,7 +264,7 @@ fn parse_admonition<'a>( return Some(match on_failure { OnFailure::Continue => Ok(Admonition { directive: Directive::Bug, - title: Some("Error rendering admonishment".to_owned()), + title: "Error rendering admonishment".to_owned(), additional_classnames: Vec::new(), collapsible: false, content: Cow::Owned(format!( @@ -286,7 +290,26 @@ Original markdown input: Some(Ok(Admonition::new(info, body))) } -fn preprocess(content: &str, on_failure: OnFailure) -> MdbookResult<String> { +fn load_defaults(ctx: &PreprocessorContext) -> Result<AdmonitionDefaults> { + let table_op = ctx.config.get("preprocessor.admonish.default"); + + Ok(if let Some(table) = table_op { + table + .to_owned() + .try_into() + .context("preprocessor.admonish.default could not be parsed from book.toml")? + } else { + Default::default() + }) +} + +fn preprocess( + content: &str, + ctx: &PreprocessorContext, + on_failure: OnFailure, +) -> MdbookResult<String> { + let admonition_defaults = load_defaults(ctx)?; + let mut id_counter = Default::default(); let mut opts = Options::empty(); opts.insert(Options::ENABLE_TABLES); @@ -297,19 +320,31 @@ fn preprocess(content: &str, on_failure: OnFailure) -> MdbookResult<String> { let mut admonish_blocks = vec![]; let events = Parser::new_ext(content, opts); + for (e, span) in events.into_offset_iter() { if let Event::Start(Tag::CodeBlock(Fenced(info_string))) = e.clone() { let span_content = &content[span.start..span.end]; - let admonition = match parse_admonition(info_string.as_ref(), span_content, on_failure) - { + + let admonition = match parse_admonition( + info_string.as_ref(), + &admonition_defaults, + span_content, + on_failure, + ) { Some(admonition) => admonition, None => continue, }; + let admonition = admonition?; let anchor_id = unique_id_from_content( - admonition.title.as_deref().unwrap_or(ANCHOR_ID_DEFAULT), + if !admonition.title.is_empty() { + &admonition.title + } else { + ANCHOR_ID_DEFAULT + }, &mut id_counter, ); + admonish_blocks.push((span, admonition.html(&anchor_id))); } } @@ -320,6 +355,7 @@ fn preprocess(content: &str, on_failure: OnFailure) -> MdbookResult<String> { let post_content = &content[span.end..]; content = format!("{}\n{}{}", pre_content, block, post_content); } + Ok(content) } @@ -328,8 +364,53 @@ mod test { use super::*; use pretty_assertions::assert_eq; + fn create_mock_context(admonish_ops: &str) -> PreprocessorContext { + let input_json = format!( + r##"[ + {{ + "root": "/path/to/book", + "config": {{ + "book": {{ + "authors": ["AUTHOR"], + "language": "en", + "multilingual": false, + "src": "src", + "title": "TITLE" + }}, + "preprocessor": {{ + "admonish": {admonish_ops} + }} + }}, + "renderer": "html", + "mdbook_version": "0.4.21" + }}, + {{ + "sections": [ + {{ + "Chapter": {{ + "name": "Chapter 1", + "content": "# Chapter 1\n", + "number": [1], + "sub_items": [], + "path": "chapter_1.md", + "source_path": "chapter_1.md", + "parent_names": [] + }} + }} + ], + "__non_exhaustive": null + }} + ]"## + ); + let input_json = input_json.as_bytes(); + + let (ctx, _) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap(); + ctx + } + fn prep(content: &str) -> String { - preprocess(content, OnFailure::Continue).unwrap() + let ctx = create_mock_context("{}"); + preprocess(content, &ctx, OnFailure::Continue).unwrap() } #[test] @@ -781,9 +862,9 @@ Bonus content! Bonus content! ``` "#; - + let ctx = create_mock_context(r#"{}"#); assert_eq!( - preprocess(content, OnFailure::Bail) + preprocess(content, &ctx, OnFailure::Bail) .unwrap_err() .to_string(), r#"Error processing admonition, bailing: @@ -821,4 +902,86 @@ Hidden assert_eq!(expected, prep(content)); } + + #[test] + fn default_toml_title() { + let content = r#"# Chapter +```admonish +A simple admonition. +``` +Text +"#; + + let expected = r##"# Chapter + +<div id="admonition-admonish" class="admonition note"> +<div class="admonition-title"> + +Admonish + +<a class="admonition-anchor-link" href="#admonition-admonish"></a> +</div> +<div> + +A simple admonition. + +</div> +</div> +Text +"##; + + let ctx = create_mock_context(r#"{"default": {"title": "Admonish"}}"#); + let preprocess_result = preprocess(content, &ctx, OnFailure::Continue).unwrap(); + assert_eq!(expected, preprocess_result); + } + + #[test] + fn empty_explicit_title_with_default() { + let content = r#"# Chapter +```admonish title="" +A simple admonition. +``` +Text +"#; + + let expected = r##"# Chapter + +<div id="admonition-default" class="admonition note"> +<div> + +A simple admonition. + +</div> +</div> +Text +"##; + + let ctx = create_mock_context(r#"{"default": {"title": "Admonish"}}"#); + let preprocess_result = preprocess(content, &ctx, OnFailure::Continue).unwrap(); + assert_eq!(expected, preprocess_result); + } + + #[test] + fn empty_explicit_title() { + let content = r#"# Chapter +```admonish title="" +A simple admonition. +``` +Text +"#; + + let expected = r##"# Chapter + +<div id="admonition-default" class="admonition note"> +<div> + +A simple admonition. + +</div> +</div> +Text +"##; + + assert_eq!(expected, prep(content)); + } } diff --git a/src/resolve.rs b/src/resolve.rs new file mode 100644 index 0000000..f676b32 --- /dev/null +++ b/src/resolve.rs @@ -0,0 +1,92 @@ +use crate::config::AdmonitionInfoRaw; +use crate::types::{AdmonitionDefaults, Directive}; +use std::str::FromStr; + +/// All information required to render an admonition. +/// +/// i.e. all configured options have been resolved at this point. +#[derive(Debug, PartialEq)] +pub(crate) struct AdmonitionInfo { + pub directive: Directive, + pub title: String, + pub additional_classnames: Vec<String>, + pub collapsible: bool, +} + +impl AdmonitionInfo { + pub fn from_info_string( + info_string: &str, + defaults: &AdmonitionDefaults, + ) -> Option<Result<Self, String>> { + AdmonitionInfoRaw::from_info_string(info_string) + .map(|raw| raw.map(|raw| Self::resolve(raw, defaults))) + } + + /// Combine the per-admonition configuration with global defaults (and + /// other logic) to resolve the values needed for rendering. + fn resolve(raw: AdmonitionInfoRaw, defaults: &AdmonitionDefaults) -> Self { + let AdmonitionInfoRaw { + directive: raw_directive, + title, + additional_classnames, + collapsible, + } = raw; + + // Use values from block, else load default value + let title = title.or_else(|| defaults.title.clone()); + let collapsible = collapsible.or(defaults.collapsible).unwrap_or_default(); + + // Load the directive (and title, if one still not given) + let (directive, title) = match (Directive::from_str(&raw_directive), title) { + (Ok(directive), None) => (directive, ucfirst(&raw_directive)), + (Err(_), None) => (Directive::Note, "Note".to_owned()), + (Ok(directive), Some(title)) => (directive, title), + (Err(_), Some(title)) => (Directive::Note, title), + }; + + Self { + directive, + title, + additional_classnames, + collapsible, + } + } +} + +/// Make the first letter of `input` upppercase. +/// +/// source: https://stackoverflow.com/a/38406885 +fn ucfirst(input: &str) -> String { + let mut chars = input.chars(); + match chars.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(), + } +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_admonition_info_from_raw() { + assert_eq!( + AdmonitionInfo::resolve( + AdmonitionInfoRaw { + directive: " ".to_owned(), + title: None, + additional_classnames: Vec::new(), + collapsible: None, + }, + &Default::default() + ), + AdmonitionInfo { + directive: Directive::Note, + title: "Note".to_owned(), + additional_classnames: Vec::new(), + collapsible: false, + } + ); + } +} diff --git a/src/types.rs b/src/types.rs index 8c0dedc..ce255d1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,16 @@ +use serde::{Deserialize, Serialize}; use std::str::FromStr; +/// Book wide defaults that may be provided by the user. +#[derive(Deserialize, Serialize, Debug, Default)] +pub(crate) struct AdmonitionDefaults { + #[serde(default)] + pub(crate) title: Option<String>, + + #[serde(default)] + pub(crate) collapsible: Option<bool>, +} + #[derive(Debug, PartialEq)] pub(crate) enum Directive { Note, |