summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShaun Hamilton <shauhami020@gmail.com>2023-04-15 14:31:03 +0000
committerTom Milligan <tom.milligan@uipath.com>2023-04-20 15:43:56 +0100
commitd2698387651431f9ef8a9d5b2d4a84c91de510c5 (patch)
treef98eb72f467a91a6cbcd3c92e4ad50e3bb532a08
parent082359e56262785fc145d32806b567c31f27fc02 (diff)
feat: add book-wide default values
-rw-r--r--CHANGELOG.md6
-rw-r--r--src/config/mod.rs87
-rw-r--r--src/config/v1.rs12
-rw-r--r--src/config/v2.rs22
-rw-r--r--src/lib.rs205
-rw-r--r--src/resolve.rs92
-rw-r--r--src/types.rs11
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
diff --git a/src/lib.rs b/src/lib.rs
index 0c48806..37bdd5e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,