summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSky <sky@sky9.dev>2023-11-15 17:34:21 -0500
committerGitHub <noreply@github.com>2023-11-15 22:34:21 +0000
commitc3207e4d16e1f44ec88baaf0730aae89ef9db8f1 (patch)
tree2102ef94838eec413dee211ece3b6438868e77a5
parentab63c90231cab8834cc2ab60bdaa06c411c1f6eb (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.md3
-rw-r--r--book/src/overview.md18
-rw-r--r--book/src/reference.md1
-rw-r--r--src/config/mod.rs11
-rw-r--r--src/config/v1.rs6
-rw-r--r--src/config/v2.rs20
-rw-r--r--src/markdown.rs217
-rw-r--r--src/parse.rs3
-rw-r--r--src/render.rs41
-rw-r--r--src/resolve.rs51
-rw-r--r--src/types.rs16
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),
+}