diff options
author | Tom Milligan <tom.milligan@uipath.com> | 2023-07-22 11:01:27 +0100 |
---|---|---|
committer | Tom Milligan <tom.milligan@uipath.com> | 2023-07-22 11:32:40 +0100 |
commit | de539cd0fdb8fcbef3b0ec81b3cf333853ae8f60 (patch) | |
tree | ca7bc2aa65397d076f7c523318f1437f8a52bccd | |
parent | 4842daea1c0f1624344fe55c5d906ccd72345203 (diff) |
internal: split up lib.rs
-rw-r--r-- | src/book_config.rs | 119 | ||||
-rw-r--r-- | src/lib.rs | 1264 | ||||
-rw-r--r-- | src/parse.rs | 86 | ||||
-rw-r--r-- | src/types.rs | 6 |
4 files changed, 208 insertions, 1267 deletions
diff --git a/src/book_config.rs b/src/book_config.rs new file mode 100644 index 0000000..2ec2263 --- /dev/null +++ b/src/book_config.rs @@ -0,0 +1,119 @@ +use anyhow::{anyhow, Context, Result}; +use mdbook::preprocess::PreprocessorContext; +use std::str::FromStr; + +use crate::types::AdmonitionDefaults; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RenderMode { + Preserve, + Strip, + Html, +} + +impl FromStr for RenderMode { + type Err = (); + + fn from_str(string: &str) -> Result<Self, ()> { + match string { + "preserve" => Ok(Self::Preserve), + "strip" => Ok(Self::Strip), + "html" => Ok(Self::Html), + _ => Err(()), + } + } +} + +impl RenderMode { + pub(crate) fn from_context( + context: &PreprocessorContext, + renderer: &str, + default: Self, + ) -> Result<Self> { + let key = format!("preprocessor.admonish.renderer.{renderer}.render_mode"); + let value = context.config.get(&key); + + // If no key set, return default + let value = if let Some(value) = value { + value + } else { + return Ok(default); + }; + + // Othersise, parse value + let value = value + .as_str() + .with_context(|| format!("Invalid value for {key}: {value:?}"))?; + + RenderMode::from_str(value).map_err(|_| anyhow!("Invalid value for {key}: {value}")) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum OnFailure { + Bail, + Continue, +} + +impl Default for OnFailure { + fn default() -> Self { + Self::Continue + } +} + +impl FromStr for OnFailure { + type Err = (); + + fn from_str(string: &str) -> Result<Self, ()> { + match string { + "bail" => Ok(Self::Bail), + "continue" => Ok(Self::Continue), + _ => Ok(Self::Continue), + } + } +} + +impl OnFailure { + pub(crate) fn from_context(context: &PreprocessorContext) -> Self { + context + .config + .get("preprocessor.admonish.on_failure") + .and_then(|value| value.as_str()) + .map(|value| OnFailure::from_str(value).unwrap_or_default()) + .unwrap_or_default() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Renderer { + Html, + Test, +} + +impl FromStr for Renderer { + type Err = (); + + fn from_str(string: &str) -> Result<Self, ()> { + match string { + "html" => Ok(Self::Html), + "test" => Ok(Self::Test), + _ => Err(()), + } + } +} + +impl AdmonitionDefaults { + pub(crate) fn from_context(ctx: &PreprocessorContext) -> Result<Self> { + const KEY: &str = "preprocessor.admonish.default"; + let table = ctx.config.get(KEY); + + Ok(if let Some(table) = table { + table + .to_owned() + .try_into() + .with_context(|| "{KEY} could not be parsed from book.toml")? + } else { + Default::default() + }) + } +} @@ -1,1264 +1,10 @@ -use anyhow::{anyhow, Context, Result}; -use mdbook::{ - book::{Book, BookItem}, - errors::Result as MdbookResult, - preprocess::{Preprocessor, PreprocessorContext}, - utils::unique_id_from_content, -}; -use pulldown_cmark::{CodeBlockKind::*, Event, Options, Parser, Tag}; -use std::{borrow::Cow, str::FromStr}; - +mod book_config; mod config; +mod markdown; mod parse; +mod preprocessor; +mod render; mod resolve; mod types; -use crate::{ - resolve::AdmonitionMeta, - types::{AdmonitionDefaults, Directive}, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RenderTextMode { - Strip, - Html, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RenderMode { - Preserve, - Strip, - Html, -} - -impl FromStr for RenderMode { - type Err = (); - - fn from_str(string: &str) -> Result<Self, ()> { - match string { - "preserve" => Ok(Self::Preserve), - "strip" => Ok(Self::Strip), - _ => Err(()), - } - } -} - -fn test_render_mode(context: &PreprocessorContext) -> Result<RenderMode> { - const TOML_KEY: &str = "preprocessor.admonish.renderer.test.render_mode"; - let value = context.config.get(TOML_KEY); - - // If no key set, return default - let value = if let Some(value) = value { - value - } else { - return Ok(RenderMode::Preserve); - }; - - // Othersise, parse value - let value = value - .as_str() - .with_context(|| format!("Invalid value for {TOML_KEY}: {value:?}"))?; - - RenderMode::from_str(value).map_err(|_| anyhow!("Invalid value for {TOML_KEY}: {value}")) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum OnFailure { - Bail, - Continue, -} - -impl Default for OnFailure { - fn default() -> Self { - Self::Continue - } -} - -impl FromStr for OnFailure { - type Err = (); - - fn from_str(string: &str) -> Result<Self, ()> { - match string { - "bail" => Ok(Self::Bail), - "continue" => Ok(Self::Continue), - _ => Ok(Self::Continue), - } - } -} - -impl OnFailure { - fn from_context(context: &PreprocessorContext) -> Self { - context - .config - .get("preprocessor.admonish.on_failure") - .and_then(|value| value.as_str()) - .map(|value| OnFailure::from_str(value).unwrap_or_default()) - .unwrap_or_default() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Renderer { - Html, - Test, -} - -impl FromStr for Renderer { - type Err = (); - - fn from_str(string: &str) -> Result<Self, ()> { - match string { - "html" => Ok(Self::Html), - "test" => Ok(Self::Test), - _ => Err(()), - } - } -} - -pub struct Admonish; - -impl Preprocessor for Admonish { - fn name(&self) -> &str { - "admonish" - } - - fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> MdbookResult<Book> { - ensure_compatible_assets_version(ctx)?; - let on_failure = OnFailure::from_context(ctx); - let admonition_defaults = load_defaults(ctx)?; - let renderer = Renderer::from_str(&ctx.renderer).map_err(|_| { - anyhow!( - "mdbook-admonish called with unsupported renderer '{}", - &ctx.renderer - ) - })?; - let render_mode = match renderer { - Renderer::Html => RenderMode::Html, - Renderer::Test => test_render_mode(ctx)?, - }; - let render_text_mode = match render_mode { - RenderMode::Preserve => return Ok(book), - RenderMode::Html => RenderTextMode::Html, - RenderMode::Strip => RenderTextMode::Strip, - }; - - let mut res = None; - book.for_each_mut(|item: &mut BookItem| { - if let Some(Err(_)) = res { - return; - } - - if let BookItem::Chapter(ref mut chapter) = *item { - res = Some( - preprocess( - &chapter.content, - on_failure, - &admonition_defaults, - render_text_mode, - ) - .map(|md| { - chapter.content = md; - }), - ); - } - }); - - res.unwrap_or(Ok(())).map(|_| book) - } - - fn supports_renderer(&self, renderer: &str) -> bool { - Renderer::from_str(renderer).is_ok() - } -} - -fn ensure_compatible_assets_version(ctx: &PreprocessorContext) -> Result<()> { - use semver::{Version, VersionReq}; - - const REQUIRES_ASSETS_VERSION: &str = std::include_str!("./REQUIRED_ASSETS_VERSION"); - let requirement = VersionReq::parse(REQUIRES_ASSETS_VERSION.trim()).unwrap(); - - const USER_ACTION: &str = "Please run `mdbook-admonish install` to update installed assets."; - const DOCS_REFERENCE: &str = "For more information, see: https://github.com/tommilligan/mdbook-admonish#semantic-versioning"; - - let version = match ctx - .config - .get("preprocessor.admonish.assets_version") - .and_then(|value| value.as_str()) - { - Some(version) => version, - None => { - return Err(anyhow!( - r#"ERROR: - Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but did not find a version. - {USER_ACTION} - {DOCS_REFERENCE}"# - )) - } - }; - - let version = Version::parse(version).unwrap(); - - if !requirement.matches(&version) { - return Err(anyhow!( - r#"ERROR: - Incompatible assets installed: required mdbook-admonish assets version '{requirement}', but found '{version}'. - {USER_ACTION} - {DOCS_REFERENCE}"# - )); - }; - Ok(()) -} - -impl Directive { - fn classname(&self) -> &'static str { - match self { - Directive::Note => "note", - Directive::Abstract => "abstract", - Directive::Info => "info", - Directive::Tip => "tip", - Directive::Success => "success", - Directive::Question => "question", - Directive::Warning => "warning", - Directive::Failure => "failure", - Directive::Danger => "danger", - Directive::Bug => "bug", - Directive::Example => "example", - Directive::Quote => "quote", - } - } -} - -#[derive(Debug, PartialEq)] -struct Admonition<'a> { - directive: Directive, - title: String, - content: Cow<'a, str>, - additional_classnames: Vec<String>, - collapsible: bool, -} - -impl<'a> Admonition<'a> { - pub fn new(info: AdmonitionMeta, content: &'a str) -> Self { - let AdmonitionMeta { - directive, - title, - additional_classnames, - collapsible, - } = info; - Self { - directive, - title, - content: Cow::Borrowed(content), - additional_classnames, - collapsible, - } - } - - fn html(&self, anchor_id: &str) -> String { - let mut additional_class = Cow::Borrowed(self.directive.classname()); - let title = &self.title; - let content = &self.content; - - let title_block = if self.collapsible { "summary" } else { "div" }; - - 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}> -"## - )) - } else { - Cow::Borrowed("") - }; - - if !self.additional_classnames.is_empty() { - let mut buffer = additional_class.into_owned(); - for additional_classname in &self.additional_classnames { - buffer.push(' '); - buffer.push_str(additional_classname); - } - - additional_class = Cow::Owned(buffer); - } - - let admonition_block = if self.collapsible { "details" } else { "div" }; - // Notes on the HTML template: - // - the additional whitespace around the content are deliberate - // In line with the commonmark spec, this allows the inner content to be - // rendered as markdown paragraphs. - format!( - r#" -<{admonition_block} id="{ANCHOR_ID_PREFIX}-{anchor_id}" class="admonition {additional_class}"> -{title_html}<div> - -{content} - -</div> -</{admonition_block}>"#, - ) - } - - /// Strips all admonish syntax, leaving the plain content of the block. - fn strip(&self) -> String { - // Add in newlines to preserve line numbering for test output - // These replace the code fences we stripped out - format!("\n{}\n", self.content) - } -} - -const ANCHOR_ID_PREFIX: &str = "admonition"; -const ANCHOR_ID_DEFAULT: &str = "default"; - -/// Given the content in the span of the code block, and the info string, -/// return `Some(Admonition)` if the code block is an admonition. -/// -/// If there is an error parsing the admonition, either: -/// -/// - Display a UI error message output in the book. -/// - If configured, break the build. -/// -/// 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>>> { - // We need to know fence details anyway for error messages - let extracted = parse::extract_admonish_body(content); - - let info = AdmonitionMeta::from_info_string(info_string, admonition_defaults)?; - let info = match info { - Ok(info) => info, - // FIXME return error messages to break build if configured - // Err(message) => return Some(Err(content)), - Err(message) => { - // Construct a fence capable of enclosing whatever we wrote for the - // actual input block - let fence = extracted.fence; - let enclosing_fence: String = std::iter::repeat(fence.character) - .take(fence.length + 1) - .collect(); - return Some(match on_failure { - OnFailure::Continue => Ok(Admonition { - directive: Directive::Bug, - title: "Error rendering admonishment".to_owned(), - additional_classnames: Vec::new(), - collapsible: false, - content: Cow::Owned(format!( - r#"Failed with: - -```log -{message} -``` - -Original markdown input: - -{enclosing_fence}markdown -{content} -{enclosing_fence} -"# - )), - }), - OnFailure::Bail => Err(anyhow!("Error processing admonition, bailing:\n{content}")), - }); - } - }; - - Some(Ok(Admonition::new(info, extracted.body))) -} - -fn load_defaults(ctx: &PreprocessorContext) -> Result<AdmonitionDefaults> { - let table = ctx.config.get("preprocessor.admonish.default"); - - Ok(if let Some(table) = table { - table - .to_owned() - .try_into() - .context("preprocessor.admonish.default could not be parsed from book.toml")? - } else { - Default::default() - }) -} - -fn preprocess( - content: &str, - on_failure: OnFailure, - admonition_defaults: &AdmonitionDefaults, - render_text_mode: RenderTextMode, -) -> MdbookResult<String> { - let mut id_counter = Default::default(); - let mut opts = Options::empty(); - opts.insert(Options::ENABLE_TABLES); - opts.insert(Options::ENABLE_FOOTNOTES); - opts.insert(Options::ENABLE_STRIKETHROUGH); - opts.insert(Options::ENABLE_TASKLISTS); - - let mut admonish_blocks = vec![]; - - let events = Parser::new_ext(content, opts); - - for (event, span) in events.into_offset_iter() { - if let Event::Start(Tag::CodeBlock(Fenced(info_string))) = event.clone() { - let span_content = &content[span.start..span.end]; - - let admonition = match parse_admonition( - info_string.as_ref(), - &admonition_defaults, - span_content, - on_failure, - ) { - Some(admonition) => admonition, - None => continue, - }; - - let admonition = admonition?; - - // Once we've identitified admonition blocks, handle them differently - // depending on our render mode - let new_content = match render_text_mode { - RenderTextMode::Html => { - let anchor_id = unique_id_from_content( - if !admonition.title.is_empty() { - &admonition.title - } else { - ANCHOR_ID_DEFAULT - }, - &mut id_counter, - ); - admonition.html(&anchor_id) - } - RenderTextMode::Strip => admonition.strip(), - }; - - admonish_blocks.push((span, new_content)); - } - } - - let mut content = content.to_string(); - for (span, block) in admonish_blocks.iter().rev() { - let pre_content = &content[..span.start]; - let post_content = &content[span.end..]; - content = format!("{}{}{}", pre_content, block, post_content); - } - - Ok(content) -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - use serde_json::{json, Value}; - - fn mock_book(content: &str) -> Book { - serde_json::from_value(json!({ - "sections": [ - { - "Chapter": { - "name": "Chapter 1", - "content": content, - "number": [1], - "sub_items": [], - "path": "chapter_1.md", - "source_path": "chapter_1.md", - "parent_names": [] - } - } - ], - "__non_exhaustive": null - })) - .unwrap() - } - - fn mock_context(admonish: &Value, renderer: &str) -> PreprocessorContext { - let value = json!({ - "root": "/path/to/book", - "config": { - "book": { - "authors": ["AUTHOR"], - "language": "en", - "multilingual": false, - "src": "src", - "title": "TITLE" - }, - "preprocessor": { - "admonish": admonish, - } - }, - "renderer": renderer, - "mdbook_version": "0.4.21" - }); - - serde_json::from_value(value).unwrap() - } - - fn prep(content: &str) -> String { - preprocess( - content, - OnFailure::Continue, - &AdmonitionDefaults::default(), - RenderTextMode::Html, - ) - .unwrap() - } - - #[test] - fn adds_admonish() { - let content = r#"# Chapter -```admonish -A simple admonition. -``` -Text -"#; - - let expected = r##"# Chapter - -<div id="admonition-note" class="admonition note"> -<div class="admonition-title"> - -Note - -<a class="admonition-anchor-link" href="#admonition-note"></a> -</div> -<div> - -A simple admonition. - -</div> -</div> -Text -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn adds_admonish_longer_code_fence() { - let content = r#"# Chapter -````admonish -```json -{} -``` -```` -Text -"#; - - let expected = r##"# Chapter - -<div id="admonition-note" class="admonition note"> -<div class="admonition-title"> - -Note - -<a class="admonition-anchor-link" href="#admonition-note"></a> -</div> -<div> - -```json -{} -``` - -</div> -</div> -Text -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn adds_admonish_directive() { - let content = r#"# Chapter -```admonish warning -A simple admonition. -``` -Text -"#; - - let expected = r##"# Chapter - -<div id="admonition-warning" class="admonition warning"> -<div class="admonition-title"> - -Warning - -<a class="admonition-anchor-link" href="#admonition-warning"></a> -</div> -<div> - -A simple admonition. - -</div> -</div> -Text -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn adds_admonish_directive_alternate() { - let content = r#"# Chapter -```admonish caution -A warning with alternate title. -``` -Text -"#; - - let expected = r##"# Chapter - -<div id="admonition-caution" class="admonition warning"> -<div class="admonition-title"> - -Caution - -<a class="admonition-anchor-link" href="#admonition-caution"></a> -</div> -<div> - -A warning with alternate title. - -</div> -</div> -Text -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn adds_admonish_directive_title() { - let content = r#"# Chapter -```admonish warning "Read **this**!" -A simple admonition. -``` -Text -"#; - - let expected = r##"# Chapter - -<div id="admonition-read-this" class="admonition warning"> -<div class="admonition-title"> - -Read **this**! - -<a class="admonition-anchor-link" href="#admonition-read-this"></a> -</div> -<div> - -A simple admonition. - -</div> -</div> -Text -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn leaves_tables_untouched() { - // Regression test. - // Previously we forgot to enable the same markdwon extensions as mdbook itself. - - let content = r#"# Heading -| Head 1 | Head 2 | -|--------|--------| -| Row 1 | Row 2 | -"#; - - let expected = r#"# Heading -| Head 1 | Head 2 | -|--------|--------| -| Row 1 | Row 2 | -"#; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn leaves_html_untouched() { - // Regression test. - // Don't remove important newlines for syntax nested inside HTML - - let content = r#"# Heading -<del> -*foo* -</del> -"#; - - let expected = r#"# Heading -<del> -*foo* -</del> -"#; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn html_in_list() { - // Regression test. - // Don't remove important newlines for syntax nested inside HTML - - let content = r#"# Heading -1. paragraph 1 - ``` - code 1 - ``` -2. paragraph 2 -"#; - - let expected = r#"# Heading -1. paragraph 1 - ``` - code 1 - ``` -2. paragraph 2 -"#; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn info_string_that_changes_length_when_parsed() { - let content = r#" -```admonish note "And \\"<i>in</i>\\" the title" -With <b>html</b> styling. -``` -hello -"#; - - let expected = r##" - -<div id="admonition-and-in-the-title" class="admonition note"> -<div class="admonition-title"> - -And "<i>in</i>" the title - -<a class="admonition-anchor-link" href="#admonition-and-in-the-title"></a> -</div> -<div> - -With <b>html</b> styling. - -</div> -</div> -hello -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn info_string_ending_in_symbol() { - let content = r#" -```admonish warning "Trademarkā¢" -Should be respected -``` -hello -"#; - - let expected = r##" - -<div id="admonition-trademark" class="admonition warning"> -<div class="admonition-title"> - -Trademarkā¢ - -<a class="admonition-anchor-link" href="#admonition-trademark"></a> -</div> -<div> - -Should be respected - -</div> -</div> -hello -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn block_with_additional_classname() { - let content = r#" -```admonish tip.my-style.other-style -Will have bonus classnames -``` -"#; - - let expected = r##" - -<div id="admonition-tip" class="admonition tip my-style other-style"> -<div class="admonition-title"> - -Tip - -<a class="admonition-anchor-link" href="#admonition-tip"></a> -</div> -<div> - -Will have bonus classnames - -</div> -</div> -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn block_with_additional_classname_and_title() { - let content = r#" -```admonish tip.my-style.other-style "Developers don't want you to know this one weird tip!" -Will have bonus classnames -``` -"#; - - let expected = r##" - -<div id="admonition-developers-dont-want-you-to-know-this-one-weird-tip" class="admonition tip my-style other-style"> -<div class="admonition-title"> - -Developers don't want you to know this one weird tip! - -<a class="admonition-anchor-link" href="#admonition-developers-dont-want-you-to-know-this-one-weird-tip"></a> -</div> -<div> - -Will have bonus classnames - -</div> -</div> -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn block_with_empty_additional_classnames_title_content() { - let content = r#" -```admonish .... "" -``` -"#; - - let expected = r##" - -<div id="admonition-default" class="admonition note"> -<div> - - - -</div> -</div> -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn unique_ids_same_title() { - let content = r#" -```admonish note "My Note" -Content zero. -``` - -```admonish note "My Note" -Content one. -``` -"#; - - let expected = r##" - -<div id="admonition-my-note" class="admonition note"> -<div class="admonition-title"> - -My Note - -<a class="admonition-anchor-link" href="#admonition-my-note"></a> -</div> -<div> - -Content zero. - -</div> -</div> - - -<div id="admonition-my-note-1" class="admonition note"> -<div class="admonition-title"> - -My Note - -<a class="admonition-anchor-link" href="#admonition-my-note-1"></a> -</div> -<div> - -Content one. - -</div> -</div> -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn v2_config_works() { - let content = r#" -```admonish tip class="my other-style" title="Article Heading" -Bonus content! -``` -"#; - - let expected = r##" - -<div id="admonition-article-heading" class="admonition tip my other-style"> -<div class="admonition-title"> - -Article Heading - -<a class="admonition-anchor-link" href="#admonition-article-heading"></a> -</div> -<div> - -Bonus content! - -</div> -</div> -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn continue_on_error_output() { - let content = r#" -```admonish title=" -Bonus content! -``` -"#; - - let expected = r##" - -<div id="admonition-error-rendering-admonishment" class="admonition bug"> -<div class="admonition-title"> - -Error rendering admonishment - -<a class="admonition-anchor-link" href="#admonition-error-rendering-admonishment"></a> -</div> -<div> - -Failed with: - -```log -TOML parsing error: TOML parse error at line 1, column 8 - | -1 | title=" - | ^ -invalid basic string - -``` - -Original markdown input: - -````markdown -```admonish title=" -Bonus content! -``` -```` - - -</div> -</div> -"##; - - assert_eq!(expected, prep(content)); - } - - #[test] - fn bail_on_error_output() { - let content = r#" -```admonish title=" -Bonus content! -``` -"#; - assert_eq!( - preprocess( - content, - OnFailure::Bail, - &AdmonitionDefaults::default(), - RenderTextMode::Html - ) - .unwrap_err() - .to_string(), - r#"Error processing admonition, bailing: -```admonish title=" -Bonus content! -```"# - .to_owned() - ) - } - - #[test] - fn run_html() { - let content = r#" -````admonish title="Title" -```rust -let x = 10; -x = 20; -``` -```` -"#; - let expected_content = r##" - -<div id="admonition-title" class="admonition note"> -<div class="admonition-title"> - -Title - -<a class="admonition-anchor-link" href="#admonition-title"></a> -</div> -<div> - -```rust -let x = 10; -x = 20; -``` |