diff options
author | Sky <sky@sky9.dev> | 2023-11-15 17:34:21 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-15 22:34:21 +0000 |
commit | c3207e4d16e1f44ec88baaf0730aae89ef9db8f1 (patch) | |
tree | 2102ef94838eec413dee211ece3b6438868e77a5 | |
parent | ab63c90231cab8834cc2ab60bdaa06c411c1f6eb (diff) |
Custom ids (#144)
* Custom ids
- You can now set custom CSS ids for admonishment blocks with the `id` field.
- You can now customize the default CSS id prefix (default is `"admonition-"`).
Co-authored-by: Tom Milligan <tom.milligan@uipath.com>
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | book/src/overview.md | 18 | ||||
-rw-r--r-- | book/src/reference.md | 1 | ||||
-rw-r--r-- | src/config/mod.rs | 11 | ||||
-rw-r--r-- | src/config/v1.rs | 6 | ||||
-rw-r--r-- | src/config/v2.rs | 20 | ||||
-rw-r--r-- | src/markdown.rs | 217 | ||||
-rw-r--r-- | src/parse.rs | 3 | ||||
-rw-r--r-- | src/render.rs | 41 | ||||
-rw-r--r-- | src/resolve.rs | 51 | ||||
-rw-r--r-- | src/types.rs | 16 |
11 files changed, 364 insertions, 23 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e775d7e..b21ab71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- You can now set custom CSS ids for admonishment blocks with the `id` field. +- You can now customize the default CSS id prefix (default is `"admonition-"`). + ## 1.13.1 ### Changed diff --git a/book/src/overview.md b/book/src/overview.md index c5984a5..dee4fc0 100644 --- a/book/src/overview.md +++ b/book/src/overview.md @@ -161,6 +161,23 @@ Will yield something like the following HTML, which you can then apply styles to </div> ``` +#### Custom CSS ID + +If you want to customize the CSS `id` field, set `id="custom-id"`. +This will ignore [`default.css_id_prefix`](reference.md#default). + +The default id is a normalized version of the admonishment's title, +prefixed with the `default.css_id_prefix`, +with an appended number if multiple blocks would have the same id. + +Setting the `id` field will *ignore* all other ids and the duplicate counter. + +```` +```admonish info title="My Info" id="my-special-info" +Link to this block with `#my-special-info` instead of the default `#admonition-my-info`. +``` +```` + #### Collapsible For a block to be initially collapsible, and then be openable, set `collapsible=true`: @@ -176,3 +193,4 @@ Will yield something like the following HTML, which you can then apply styles to ```admonish collapsible=true Content will be hidden initially. ``` + diff --git a/book/src/reference.md b/book/src/reference.md index 2228425..e8d12c0 100644 --- a/book/src/reference.md +++ b/book/src/reference.md @@ -38,6 +38,7 @@ Subfields: - `default.title` (optional): Title to use for blocks. Defaults to the directive used in titlecase. - `default.collapsible` (optional, default: `false`): Make blocks collapsible by default when set to `true`. +- `default.css_id_prefix` (optional, default: `"admonition-"`): The default css id prefix to add to the id of all blocks. Ignored on blocks with an `id` field. ### `renderer` diff --git a/src/config/mod.rs b/src/config/mod.rs index 4029426..b931f36 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -9,6 +9,7 @@ mod v2; pub(crate) struct InstanceConfig { pub(crate) directive: String, pub(crate) title: Option<String>, + pub(crate) id: Option<String>, pub(crate) additional_classnames: Vec<String>, pub(crate) collapsible: Option<bool>, } @@ -69,18 +70,22 @@ mod test { InstanceConfig { directive: "note".to_owned(), title: None, + id: None, additional_classnames: vec!["additional-classname".to_owned()], collapsible: None, } ); // v2 syntax is supported assert_eq!( - InstanceConfig::from_info_string(r#"admonish title="Custom Title" type="question""#) - .unwrap() - .unwrap(), + InstanceConfig::from_info_string( + r#"admonish title="Custom Title" type="question" id="my-id""# + ) + .unwrap() + .unwrap(), InstanceConfig { directive: "question".to_owned(), title: Some("Custom Title".to_owned()), + id: Some("my-id".to_owned()), additional_classnames: Vec::new(), collapsible: None, } diff --git a/src/config/v1.rs b/src/config/v1.rs index 20c9645..9a37b41 100644 --- a/src/config/v1.rs +++ b/src/config/v1.rs @@ -52,6 +52,7 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig, Ok(InstanceConfig { directive: directive.to_owned(), title, + id: None, additional_classnames, collapsible: None, }) @@ -69,6 +70,7 @@ mod test { InstanceConfig { directive: "".to_owned(), title: None, + id: None, additional_classnames: Vec::new(), collapsible: None, } @@ -78,6 +80,7 @@ mod test { InstanceConfig { directive: "".to_owned(), title: None, + id: None, additional_classnames: Vec::new(), collapsible: None, } @@ -87,6 +90,7 @@ mod test { InstanceConfig { directive: "unknown".to_owned(), title: None, + id: None, additional_classnames: Vec::new(), collapsible: None, } @@ -96,6 +100,7 @@ mod test { InstanceConfig { directive: "note".to_owned(), title: None, + id: None, additional_classnames: Vec::new(), collapsible: None, } @@ -105,6 +110,7 @@ mod test { InstanceConfig { directive: "note".to_owned(), title: None, + id: None, additional_classnames: vec!["additional-classname".to_owned()], collapsible: None, } diff --git a/src/config/v2.rs b/src/config/v2.rs index 7db6aec..c01dbaa 100644 --- a/src/config/v2.rs +++ b/src/config/v2.rs @@ -10,6 +10,8 @@ struct UserInput { #[serde(default)] title: Option<String>, #[serde(default)] + id: Option<String>, + #[serde(default)] class: Option<String>, #[serde(default)] collapsible: Option<bool>, @@ -88,6 +90,7 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig, Ok(InstanceConfig { directive: config.r#type.unwrap_or_default(), title: config.title, + id: config.id, additional_classnames, collapsible: config.collapsible, }) @@ -105,6 +108,7 @@ mod test { InstanceConfig { directive: "".to_owned(), title: None, + id: None, additional_classnames: Vec::new(), collapsible: None, } @@ -114,6 +118,7 @@ mod test { InstanceConfig { directive: "".to_owned(), title: None, + id: None, additional_classnames: Vec::new(), collapsible: None, } @@ -126,6 +131,7 @@ mod test { InstanceConfig { directive: "note".to_owned(), title: Some("Никита".to_owned()), + id: None, additional_classnames: vec!["additional".to_owned(), "classname".to_owned()], collapsible: Some(true), } @@ -136,6 +142,7 @@ mod test { InstanceConfig { directive: "".to_owned(), title: None, + id: None, additional_classnames: Vec::new(), collapsible: None, } @@ -146,6 +153,7 @@ mod test { InstanceConfig { directive: "info".to_owned(), title: None, + id: None, additional_classnames: Vec::new(), collapsible: None, } @@ -156,10 +164,22 @@ mod test { 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(), + 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()); } diff --git a/src/markdown.rs b/src/markdown.rs index 676b45d..f12e000 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -47,7 +47,7 @@ pub(crate) fn preprocess( // Once we've identitified admonition blocks, handle them differently // depending on our render mode let new_content = match render_text_mode { - RenderTextMode::Html => admonition.html_with_unique_ids(&mut id_counter), + RenderTextMode::Html => admonition.html(&mut id_counter), RenderTextMode::Strip => admonition.strip(), }; @@ -732,6 +732,7 @@ Text OnFailure::Continue, &AdmonitionDefaults { title: Some("Admonish".to_owned()), + css_id_prefix: None, collapsible: false, }, RenderTextMode::Html, @@ -766,6 +767,7 @@ Text OnFailure::Continue, &AdmonitionDefaults { title: Some("Admonish".to_owned()), + css_id_prefix: None, collapsible: false, }, RenderTextMode::Html, @@ -799,6 +801,219 @@ Text } #[test] + fn standard_custom_id() { + let content = r#"# Chapter +```admonish check id="yay-custom-id" +A simple admonition. +``` +Text +"#; + + let expected = r##"# Chapter + +<div id="yay-custom-id" class="admonition admonish-success"> +<div class="admonition-title"> + +Check + +<a class="admonition-anchor-link" href="#yay-custom-id"></a> +</div> +<div> + +A simple admonition. + +</div> +</div> +Text +"##; + + assert_eq!(expected, prep(content)); + } + + #[test] + fn no_custom_id_default_prefix() { + let content = r#"# Chapter +```admonish check +A simple admonition. +``` +Text +"#; + + let expected = r##"# Chapter + +<div id="admonition-check" class="admonition admonish-success"> +<div class="admonition-title"> + +Check + +<a class="admonition-anchor-link" href="#admonition-check"></a> +</div> +<div> + +A simple admonition. + +</div> +</div> +Text +"##; + + assert_eq!(expected, prep(content)); + } + + #[test] + fn no_custom_id_default_prefix_custom_title() { + let content = r#"# Chapter +```admonish check title="Check Mark" +A simple admonition. +``` +Text +"#; + + let expected = r##"# Chapter + +<div id="admonition-check-mark" class="admonition admonish-success"> +<div class="admonition-title"> + +Check Mark + +<a class="admonition-anchor-link" href="#admonition-check-mark"></a> +</div> +<div> + +A simple admonition. + +</div> +</div> +Text +"##; + + assert_eq!(expected, prep(content)); + } + + #[test] + fn empty_default_id_prefix() { + let content = r#"# Chapter +```admonish info +A simple admonition. +``` +Text +"#; + + let expected = r##"# Chapter + +<div id="info" class="admonition admonish-info"> +<div class="admonition-title"> + +Info + +<a class="admonition-anchor-link" href="#info"></a> +</div> +<div> + +A simple admonition. + +</div> +</div> +Text +"##; + + let preprocess_result = preprocess( + content, + OnFailure::Continue, + &AdmonitionDefaults { + title: Some("Info".to_owned()), + css_id_prefix: Some("".to_owned()), + collapsible: false, + }, + RenderTextMode::Html, + ) + .unwrap(); + assert_eq!(expected, preprocess_result); + } + + #[test] + fn custom_id_prefix_custom_title() { + let content = r#"# Chapter +```admonish info title="My Title" +A simple admonition. +``` +Text +"#; + + let expected = r##"# Chapter + +<div id="prefix-my-title" class="admonition admonish-info"> +<div class="admonition-title"> + +My Title + +<a class="admonition-anchor-link" href="#prefix-my-title"></a> +</div> +<div> + +A simple admonition. + +</div> +</div> +Text +"##; + + let preprocess_result = preprocess( + content, + OnFailure::Continue, + &AdmonitionDefaults { + title: Some("Info".to_owned()), + css_id_prefix: Some("prefix-".to_owned()), + collapsible: false, + }, + RenderTextMode::Html, + ) + .unwrap(); + assert_eq!(expected, preprocess_result); + } + + #[test] + fn custom_id_custom_title() { + let content = r#"# Chapter +```admonish info title="My Title" id="my-section-id" +A simple admonition. +``` +Text +"#; + + let expected = r##"# Chapter + +<div id="my-section-id" class="admonition admonish-info"> +<div class="admonition-title"> + +My Title + +<a class="admonition-anchor-link" href="#my-section-id"></a> +</div> +<div> + +A simple admonition. + +</div> +</div> +Text +"##; + + let preprocess_result = preprocess( + content, + OnFailure::Continue, + &AdmonitionDefaults { + title: Some("Info".to_owned()), + css_id_prefix: Some("ignored-prefix-".to_owned()), + collapsible: false, + }, + RenderTextMode::Html, + ) + .unwrap(); + assert_eq!(expected, preprocess_result); + } + + #[test] fn list_embed() { let content = r#"# Chapter diff --git a/src/parse.rs b/src/parse.rs index e06b234..dd85119 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,7 +6,7 @@ use crate::{ book_config::OnFailure, render::Admonition, resolve::AdmonitionMeta, - types::{AdmonitionDefaults, Directive}, + types::{AdmonitionDefaults, CssId, Directive}, }; /// Given the content in the span of the code block, and the info string, @@ -46,6 +46,7 @@ pub(crate) fn parse_admonition<'a>( Ok(Admonition { directive: Directive::Bug, title: "Error rendering admonishment".to_owned(), + css_id: CssId::Prefix("admonition-".to_owned()), additional_classnames: Vec::new(), collapsible: false, content: Cow::Owned(format!( diff --git a/src/render.rs b/src/render.rs index 61d384f..d5f49f6 100644 --- a/src/render.rs +++ b/src/render.rs @@ -3,7 +3,10 @@ use std::borrow::Cow; use std::collections::HashMap; pub use crate::preprocessor::Admonish; -use crate::{resolve::AdmonitionMeta, types::Directive}; +use crate::{ + resolve::AdmonitionMeta, + types::{CssId, Directive}, +}; impl Directive { fn classname(&self) -> &'static str { @@ -29,6 +32,7 @@ pub(crate) struct Admonition<'a> { pub(crate) directive: Directive, pub(crate) title: String, pub(crate) content: Cow<'a, str>, + pub(crate) css_id: CssId, pub(crate) additional_classnames: Vec<String>, pub(crate) collapsible: bool, pub(crate) indent: usize, @@ -39,6 +43,7 @@ impl<'a> Admonition<'a> { let AdmonitionMeta { directive, title, + css_id, additional_classnames, collapsible, } = info; @@ -46,25 +51,30 @@ impl<'a> Admonition<'a> { directive, title, content: Cow::Borrowed(content), + css_id, additional_classnames, collapsible, indent, } } - pub(crate) fn html_with_unique_ids(&self, id_counter: &mut HashMap<String, usize>) -> String { - let anchor_id = unique_id_from_content( - if !self.title.is_empty() { - &self.title - } else { - ANCHOR_ID_DEFAULT - }, - id_counter, - ); - self.html(&anchor_id) - } + pub(crate) fn html(&self, id_counter: &mut HashMap<String, usize>) -> String { + let anchor_id = match &self.css_id { + CssId::Verbatim(id) => Cow::Borrowed(id), + CssId::Prefix(prefix) => { + let id = unique_id_from_content( + if !self.title.is_empty() { + &self.title + } else { + ANCHOR_ID_DEFAULT + }, + id_counter, + ); + + Cow::Owned(format!("{}{}", prefix, id)) + } + }; - fn html(&self, anchor_id: &str) -> String { let mut additional_class = Cow::Borrowed(self.directive.classname()); let title = &self.title; let content = &self.content; @@ -78,7 +88,7 @@ impl<'a> Admonition<'a> { {indent} {indent}{title} {indent} -{indent}<a class="admonition-anchor-link" href="#{ANCHOR_ID_PREFIX}-{anchor_id}"></a> +{indent}<a class="admonition-anchor-link" href="#{anchor_id}"></a> {indent}</{title_block}> "## )) @@ -103,7 +113,7 @@ impl<'a> Admonition<'a> { // rendered as markdown paragraphs. format!( r#" -{indent}<{admonition_block} id="{ANCHOR_ID_PREFIX}-{anchor_id}" class="admonition {additional_class}"> +{indent}<{admonition_block} id="{anchor_id}" class="admonition {additional_class}"> {title_html}{indent}<div> {indent} {indent}{content} @@ -121,5 +131,4 @@ impl<'a> Admonition<'a> { } } -const ANCHOR_ID_PREFIX: &str = "admonition"; const ANCHOR_ID_DEFAULT: &str = "default"; diff --git a/src/resolve.rs b/src/resolve.rs index 93bde90..a933f61 100644 --- a/src/resolve.rs +++ b/src/resolve.rs @@ -1,5 +1,5 @@ use crate::config::InstanceConfig; -use crate::types::{AdmonitionDefaults, Directive}; +use crate::types::{AdmonitionDefaults, CssId, Directive}; use std::str::FromStr; /// All information required to render an admonition. @@ -9,6 +9,7 @@ use std::str::FromStr; pub(crate) struct AdmonitionMeta { pub directive: Directive, pub title: String, + pub css_id: CssId, pub additional_classnames: Vec<String>, pub collapsible: bool, } @@ -28,6 +29,7 @@ impl AdmonitionMeta { let InstanceConfig { directive: raw_directive, title, + id, additional_classnames, collapsible, } = raw; @@ -44,9 +46,22 @@ impl AdmonitionMeta { (Err(_), Some(title)) => (Directive::Note, title), }; + let css_id = if let Some(verbatim) = id { + CssId::Verbatim(verbatim) + } else { + const DEFAULT_CSS_ID_PREFIX: &str = "admonition-"; + CssId::Prefix( + defaults + .css_id_prefix + .clone() + .unwrap_or_else(|| DEFAULT_CSS_ID_PREFIX.to_owned()), + ) + }; + Self { directive, title, + css_id, additional_classnames, collapsible, } @@ -64,7 +79,7 @@ fn format_directive_title(input: &str) -> String { } } -/// Make the first letter of `input` upppercase. +/// Make the first letter of `input` uppercase. /// /// source: https://stackoverflow.com/a/38406885 fn uppercase_first(input: &str) -> String { @@ -99,6 +114,7 @@ mod test { InstanceConfig { directive: " ".to_owned(), title: None, + id: None, additional_classnames: Vec::new(), collapsible: None, }, @@ -107,6 +123,7 @@ mod test { AdmonitionMeta { directive: Directive::Note, title: "Note".to_owned(), + css_id: CssId::Prefix("admonition-".to_owned()), additional_classnames: Vec::new(), collapsible: false, } @@ -120,17 +137,47 @@ mod test { InstanceConfig { directive: " ".to_owned(), title: None, + id: None, + additional_classnames: Vec::new(), + collapsible: None, + }, + &AdmonitionDefaults { + title: Some("Important!!!".to_owned()), + css_id_prefix: Some("custom-prefix-".to_owned()), + collapsible: true, + }, + ), + AdmonitionMeta { + directive: Directive::Note, + title: "Important!!!".to_owned(), + css_id: CssId::Prefix("custom-prefix-".to_owned()), + additional_classnames: Vec::new(), + collapsible: true, + } + ); + } + + #[test] + fn test_admonition_info_from_raw_with_defaults_and_custom_id() { + assert_eq!( + AdmonitionMeta::resolve( + InstanceConfig { + directive: " ".to_owned(), + title: None, + id: Some("my-custom-id".to_owned()), additional_classnames: Vec::new(), collapsible: None, }, &AdmonitionDefaults { title: Some("Important!!!".to_owned()), + css_id_prefix: Some("ignored-custom-prefix-".to_owned()), collapsible: true, }, ), AdmonitionMeta { directive: Directive::Note, title: "Important!!!".to_owned(), + css_id: CssId::Verbatim("my-custom-id".to_owned()), additional_classnames: Vec::new(), collapsible: true, } diff --git a/src/types.rs b/src/types.rs index 669eab5..8ea233a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,12 +3,16 @@ 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>, #[serde(default)] pub(crate) collapsible: bool, + + #[serde(default)] + pub(crate) css_id_prefix: Option<String>, } #[derive(Debug, PartialEq)] @@ -54,3 +58,15 @@ pub(crate) enum RenderTextMode { Strip, Html, } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum CssId { + /// id="my-id" in the admonishment + /// + /// used directly for the id field + Verbatim(String), + /// the prefix from default.css_id_prefix (or "admonish-" if not specified) + /// + /// will generate the rest of the id based on the title + Prefix(String), +} |