#[cfg(not(target_arch = "wasm32"))]
use crate::error::HtmlError;
use crate::{
accessibility::add_aria_attributes,
extract_front_matter,
performance::minify_html_string,
seo::{escape_html, generate_structured_data_from_doc},
utils::generate_table_of_contents,
Result,
};
#[cfg(target_arch = "wasm32")]
use comrak::Options;
use log::warn;
#[cfg(not(target_arch = "wasm32"))]
use mdx_gen::{process_markdown, MarkdownOptions, Options};
use once_cell::sync::Lazy;
#[cfg(not(target_arch = "wasm32"))]
use regex::Regex;
#[cfg(not(target_arch = "wasm32"))]
use std::borrow::Cow;
use std::error::Error;
use std::fmt;
static BASE_COMRAK_OPTIONS: Lazy<Options<'static>> = Lazy::new(|| {
let mut opts = Options::default();
opts.extension.strikethrough = true;
opts.extension.table = true;
opts.extension.autolink = true;
opts.extension.tasklist = true;
opts.extension.superscript = true;
opts
});
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticLevel {
Info,
Warning,
Error,
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub step: &'static str,
pub level: DiagnosticLevel,
pub message: String,
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{:?}] {}: {}", self.level, self.step, self.message)
}
}
#[derive(Debug, Clone)]
pub struct HtmlOutput {
pub html: String,
pub diagnostics: Vec<Diagnostic>,
}
#[cfg(not(target_arch = "wasm32"))]
static CUSTOM_CLASS_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r":::(\w+)\n([\s\S]*?)\n:::")
.expect("static CUSTOM_CLASS_REGEX must compile")
});
#[cfg(not(target_arch = "wasm32"))]
static IMAGE_CLASS_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"!\[(.*?)\]\((.*?)\)\.class="(.*?)""#)
.expect("static IMAGE_CLASS_REGEX must compile")
});
pub fn generate_html(
markdown: &str,
config: &crate::HtmlConfig,
) -> Result<String> {
generate_html_with_diagnostics(markdown, config).map(|o| o.html)
}
pub fn generate_html_with_diagnostics(
markdown: &str,
config: &crate::HtmlConfig,
) -> Result<HtmlOutput> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
let mut html = markdown_to_html_impl(markdown, config)?;
if config.allow_unsafe_html && config.sanitize_html {
html = ammonia::clean(&html);
diagnostics.push(Diagnostic {
step: "sanitization",
level: DiagnosticLevel::Info,
message: "HTML sanitized via ammonia".to_string(),
});
}
if config.add_aria_attributes {
match add_aria_attributes(&html, None) {
Ok(enhanced) => {
html = enhanced;
diagnostics.push(Diagnostic {
step: "accessibility",
level: DiagnosticLevel::Info,
message: "ARIA attributes added".to_string(),
});
}
Err(e) => {
let d = Diagnostic {
step: "accessibility",
level: DiagnosticLevel::Error,
message: format!("ARIA enhancement skipped: {e}"),
};
warn!("{d}");
diagnostics.push(d);
}
}
}
if config.generate_toc {
match generate_table_of_contents(&html) {
Ok(toc) => {
html = html.replace("[[TOC]]", &toc);
diagnostics.push(Diagnostic {
step: "toc",
level: DiagnosticLevel::Info,
message: "Table of contents injected".to_string(),
});
}
Err(e) => {
let d = Diagnostic {
step: "toc",
level: DiagnosticLevel::Error,
message: format!(
"Table of contents generation failed: {e}"
),
};
warn!("{d}");
diagnostics.push(d);
}
}
}
#[cfg(feature = "math")]
if config.enable_math {
let before_len = html.len();
html = crate::math::convert_math(&html);
if html.len() != before_len {
diagnostics.push(Diagnostic {
step: "math",
level: DiagnosticLevel::Info,
message: "LaTeX math rendered to MathML".to_string(),
});
}
}
if config.enable_diagrams {
let before_len = html.len();
html = crate::math::rewrite_mermaid_blocks(&html);
if html.len() != before_len {
diagnostics.push(Diagnostic {
step: "diagrams",
level: DiagnosticLevel::Info,
message:
"Mermaid blocks rewritten for client-side rendering"
.to_string(),
});
}
}
let document = scraper::Html::parse_document(&html);
let mut json_ld_fragment = String::new();
if config.generate_structured_data {
match generate_structured_data_from_doc(&document, None) {
Ok(json_ld) => {
json_ld_fragment = json_ld;
diagnostics.push(Diagnostic {
step: "structured_data",
level: DiagnosticLevel::Info,
message: "JSON-LD structured data generated"
.to_string(),
});
}
Err(e) => {
let d = Diagnostic {
step: "structured_data",
level: DiagnosticLevel::Error,
message: format!(
"Structured data generation failed: {e}"
),
};
warn!("{d}");
diagnostics.push(d);
}
}
}
if config.generate_full_document {
let title = extract_first_heading_from_doc(&document);
html = wrap_full_document(
&html,
&json_ld_fragment,
title.as_deref(),
config,
);
} else {
if !json_ld_fragment.is_empty() {
html.push_str(&json_ld_fragment);
}
if config.language != crate::constants::DEFAULT_LANGUAGE {
html = format!(
"<div lang=\"{}\">{}</div>",
escape_html(&config.language),
html
);
}
}
if config.minify_output {
let before_len = html.len();
match minify_html_string(&html) {
Ok(minified) => {
let saved = before_len.saturating_sub(minified.len());
html = minified;
diagnostics.push(Diagnostic {
step: "minification",
level: DiagnosticLevel::Info,
message: format!(
"Minified: saved {} bytes ({:.0}%)",
saved,
if before_len > 0 {
saved as f64 / before_len as f64 * 100.0
} else {
0.0
}
),
});
}
Err(e) => {
let d = Diagnostic {
step: "minification",
level: DiagnosticLevel::Error,
message: format!("Minification failed: {e}"),
};
warn!("{d}");
diagnostics.push(d);
}
}
}
Ok(HtmlOutput { html, diagnostics })
}
fn wrap_full_document(
body: &str,
json_ld: &str,
title: Option<&str>,
config: &crate::HtmlConfig,
) -> String {
let lang = escape_html(&config.language);
let mut head = String::from("<meta charset=\"utf-8\">");
if let Some(t) = title {
head.push_str(&format!("<title>{}</title>", escape_html(t)));
}
if !json_ld.is_empty() {
head.push_str(json_ld);
}
format!(
"<!DOCTYPE html>\n<html lang=\"{lang}\">\n<head>{head}</head>\n<body>\n{body}\n</body>\n</html>"
)
}
static H1_SELECTOR: Lazy<scraper::Selector> = Lazy::new(|| {
scraper::Selector::parse("h1")
.expect("static H1_SELECTOR must parse")
});
fn extract_first_heading_from_doc(
document: &scraper::Html,
) -> Option<String> {
document
.select(&H1_SELECTOR)
.next()
.map(|el| el.text().collect::<String>())
}
pub fn markdown_to_html_with_extensions(
markdown: &str,
) -> Result<String> {
markdown_to_html_impl(markdown, &crate::HtmlConfig::default())
}
#[cfg(not(target_arch = "wasm32"))]
fn markdown_to_html_impl(
markdown: &str,
config: &crate::HtmlConfig,
) -> Result<String> {
let content_without_front_matter = extract_front_matter(markdown)
.unwrap_or_else(|_| markdown.to_string());
let markdown_with_classes = add_custom_classes(
&content_without_front_matter,
config.allow_unsafe_html,
);
let markdown_with_images =
process_images_with_classes(&markdown_with_classes);
let mut comrak_options = BASE_COMRAK_OPTIONS.clone();
comrak_options.render.r#unsafe = config.allow_unsafe_html;
let mut md_options = MarkdownOptions::default()
.with_comrak_options(comrak_options)
.with_syntax_highlighting(config.enable_syntax_highlighting);
if let Some(ref theme) = config.syntax_theme {
md_options = md_options.with_custom_theme(theme.clone());
}
process_markdown(&markdown_with_images, &md_options).map_err(
|err| HtmlError::markdown_conversion(err.to_string(), None),
)
}
#[cfg(target_arch = "wasm32")]
fn markdown_to_html_impl(
markdown: &str,
config: &crate::HtmlConfig,
) -> Result<String> {
let content_without_front_matter = extract_front_matter(markdown)
.unwrap_or_else(|_| markdown.to_string());
let mut opts = BASE_COMRAK_OPTIONS.clone();
opts.render.r#unsafe = config.allow_unsafe_html;
Ok(comrak::markdown_to_html(
&content_without_front_matter,
&opts,
))
}
#[cfg(not(target_arch = "wasm32"))]
fn add_custom_classes(
markdown: &str,
allow_unsafe_html: bool,
) -> Cow<'_, str> {
CUSTOM_CLASS_REGEX.replace_all(
markdown,
|caps: ®ex::Captures| {
let class_name = &caps[1];
let block_content = &caps[2];
let inline_html = match process_markdown_inline_impl(
block_content,
allow_unsafe_html,
) {
Ok(html) => html,
Err(_) => block_content.to_string(),
};
format!(
"<div class=\"{}\">{}</div>",
class_name, inline_html
)
},
)
}
pub fn process_markdown_inline(
content: &str,
) -> std::result::Result<String, Box<dyn Error>> {
process_markdown_inline_impl(content, false)
}
#[cfg(not(target_arch = "wasm32"))]
fn process_markdown_inline_impl(
content: &str,
allow_unsafe_html: bool,
) -> std::result::Result<String, Box<dyn Error>> {
let mut comrak_opts = BASE_COMRAK_OPTIONS.clone();
comrak_opts.render.r#unsafe = allow_unsafe_html;
let options =
MarkdownOptions::default().with_comrak_options(comrak_opts);
Ok(process_markdown(content, &options)?)
}
#[cfg(target_arch = "wasm32")]
fn process_markdown_inline_impl(
content: &str,
allow_unsafe_html: bool,
) -> std::result::Result<String, Box<dyn Error>> {
let mut opts = BASE_COMRAK_OPTIONS.clone();
opts.render.r#unsafe = allow_unsafe_html;
Ok(comrak::markdown_to_html(content, &opts))
}
#[cfg(not(target_arch = "wasm32"))]
fn process_images_with_classes(markdown: &str) -> Cow<'_, str> {
IMAGE_CLASS_REGEX.replace_all(markdown, |caps: ®ex::Captures| {
format!(
r#"<img src="{}" alt="{}" class="{}" />"#,
escape_html(&caps[2]), escape_html(&caps[1]), escape_html(&caps[3]), )
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::HtmlConfig;
#[test]
fn test_generate_html_basic() {
let markdown = "# Hello, world!\n\nThis is a test.";
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<h1>Hello, world!</h1>"));
assert!(html.contains("<p>This is a test.</p>"));
}
#[test]
fn test_markdown_to_html_with_extensions() {
let markdown = r"
| Header 1 | Header 2 |
| -------- | -------- |
| Row 1 | Row 2 |
";
let result = markdown_to_html_with_extensions(markdown);
assert!(result.is_ok());
let html = result.unwrap();
println!("{}", html);
assert!(html.contains("<div class=\"table-responsive\"><table class=\"table\">"), "Table element not found");
assert!(
html.contains("<th>Header 1</th>"),
"Table header not found"
);
assert!(
html.contains("<td class=\"text-left\">Row 1</td>"),
"Table row not found"
);
}
#[test]
fn test_generate_html_empty() {
let markdown = "";
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.is_empty());
}
#[test]
fn test_generate_html_invalid_markdown() {
let markdown = "# Unclosed header\nSome **unclosed bold";
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
println!("{}", html);
assert!(
html.contains("<h1>Unclosed header</h1>"),
"Header not found"
);
assert!(
html.contains("<p>Some **unclosed bold</p>"),
"Unclosed bold tag not properly handled"
);
}
#[test]
fn test_generate_html_complex() {
let markdown = r#"
# Header
## Subheader
Some `inline code` and a [link](https://example.com).
```rust
fn main() {
println!("Hello, world!");
}
```
1. First item
2. Second item
"#;
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
println!("{}", html);
assert!(
html.contains("<h1>Header</h1>"),
"H1 Header not found"
);
assert!(
html.contains("<h2>Subheader</h2>"),
"H2 Header not found"
);
assert!(
html.contains("<code>inline code</code>"),
"Inline code not found"
);
assert!(
html.contains(r#"<a href="https://example.com">link</a>"#),
"Link not found"
);
assert!(
html.contains(r#"<code class="language-rust">"#),
"Code block with language-rust class not found"
);
assert!(
html.contains(r#"<span style="color:#b48ead;">fn </span>"#),
"`fn` keyword with syntax highlighting not found"
);
assert!(
html.contains(
r#"<span style="color:#8fa1b3;">main</span>"#
),
"`main` function name with syntax highlighting not found"
);
assert!(
html.contains("<li>First item</li>"),
"First item not found"
);
assert!(
html.contains("<li>Second item</li>"),
"Second item not found"
);
}
#[test]
fn test_generate_html_with_valid_front_matter() {
let markdown = r#"---
title: Test
author: Jane Doe
---
# Hello, world!"#;
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<h1>Hello, world!</h1>"));
}
#[test]
fn test_generate_html_with_invalid_front_matter() {
let markdown = r#"---
title Test
author: Jane Doe
---
# Hello, world!"#;
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(
result.is_ok(),
"Invalid front matter should be ignored"
);
let html = result.unwrap();
assert!(html.contains("<h1>Hello, world!</h1>"));
}
#[test]
fn test_generate_html_large_input() {
let markdown = "# Large Markdown\n\n".repeat(10_000);
let config = HtmlConfig::default();
let result = generate_html(&markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<h1>Large Markdown</h1>"));
}
#[test]
fn test_generate_html_with_custom_markdown_options() {
let markdown = "**Bold text**";
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<strong>Bold text</strong>"));
}
#[test]
fn test_generate_html_with_unsupported_elements() {
let markdown = "::: custom_block\nContent\n:::";
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("::: custom_block"));
}
#[test]
fn test_markdown_to_html_with_conversion_error() {
let markdown = "# Unclosed header\nSome **unclosed bold";
let result = markdown_to_html_with_extensions(markdown);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<p>Some **unclosed bold</p>"));
}
#[test]
fn test_generate_html_whitespace_only() {
let markdown = " \n ";
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
assert!(
html.is_empty(),
"Whitespace-only Markdown should produce empty HTML"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_markdown_to_html_with_custom_comrak_options() {
let markdown = "^^Superscript^^\n\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Row 1 | Row 2 |";
let mut comrak_options = Options::default();
comrak_options.extension.superscript = true;
comrak_options.extension.table = true;
let options = MarkdownOptions::default()
.with_comrak_options(comrak_options.clone());
let content_without_front_matter =
extract_front_matter(markdown)
.unwrap_or(markdown.to_string());
println!("Comrak options: {:?}", comrak_options);
let result =
process_markdown(&content_without_front_matter, &options);
match result {
Ok(ref html) => {
assert!(
html.contains("<sup>Superscript</sup>"),
"Superscript not found in HTML output"
);
assert!(
html.contains("<table"),
"Table element not found in HTML output"
);
}
Err(err) => {
panic!(
"Failed to process Markdown with custom Options: {:?}",
err
);
}
}
}
#[test]
fn test_generate_html_with_default_config() {
let markdown = "# Default Configuration Test";
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<h1>Default Configuration Test</h1>"));
}
#[test]
fn test_generate_html_with_custom_front_matter_delimiter() {
let markdown = r#";;;;
title: Custom
author: John Doe
;;;;
# Custom Front Matter Delimiter"#;
let config = HtmlConfig::default();
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<h1>Custom Front Matter Delimiter</h1>"));
}
#[test]
fn test_generate_html_with_task_list() {
let markdown = r"
- [x] Task 1
- [ ] Task 2
";
let result = markdown_to_html_with_extensions(markdown);
assert!(result.is_ok());
let html = result.unwrap();
println!("Generated HTML:\n{}", html);
assert!(
html.contains(r#"<li><input type="checkbox" checked="" disabled="" /> Task 1</li>"#),
"Task 1 checkbox not rendered as expected"
);
assert!(
html.contains(r#"<li><input type="checkbox" disabled="" /> Task 2</li>"#),
"Task 2 checkbox not rendered as expected"
);
}
#[test]
fn test_generate_html_with_large_table() {
let header =
"| Header 1 | Header 2 |\n| -------- | -------- |\n";
let rows = "| Row 1 | Row 2 |\n".repeat(1000);
let markdown = format!("{}{}", header, rows);
let result = markdown_to_html_with_extensions(&markdown);
assert!(result.is_ok());
let html = result.unwrap();
let row_count = html.matches("<tr>").count();
assert_eq!(
row_count, 1001,
"Incorrect number of rows: {}",
row_count
); }
#[test]
fn test_generate_html_with_special_characters() {
let markdown = r#"Markdown with special characters: <, >, &, "quote", 'single-quote'."#;
let result = markdown_to_html_with_extensions(markdown);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<"), "Less than sign not escaped");
assert!(html.contains(">"), "Greater than sign not escaped");
assert!(html.contains("&"), "Ampersand not escaped");
assert!(html.contains("""), "Double quote not escaped");
assert!(
html.contains("'") || html.contains("'"),
"Single quote not handled as expected"
);
}
#[test]
fn test_generate_html_with_invalid_markdown_syntax() {
let markdown =
r"# Invalid Markdown <unexpected> [bad](url <here)";
let result = markdown_to_html_with_extensions(markdown);
assert!(result.is_ok());
let html = result.unwrap();
println!("Generated HTML:\n{}", html);
assert!(html.contains("<h1>"), "Header tag should be present");
}
#[test]
fn test_generate_html_mixed_markdown() {
let markdown = r"# Valid Header
Some **bold text** followed by invalid Markdown:
~~strikethrough~~ without a closing tag.";
let result = markdown_to_html_with_extensions(markdown);
assert!(result.is_ok());
let html = result.unwrap();
assert!(
html.contains("<h1>Valid Header</h1>"),
"Header not found"
);
assert!(
html.contains("<strong>bold text</strong>"),
"Bold text not rendered correctly"
);
assert!(
html.contains("<del>strikethrough</del>"),
"Strikethrough not rendered correctly"
);
}
#[test]
fn test_generate_html_deeply_nested_content() {
let markdown = r"
1. Level 1
1.1. Level 2
1.1.1. Level 3
1.1.1.1. Level 4
";
let result = markdown_to_html_with_extensions(markdown);
assert!(result.is_ok());
let html = result.unwrap();
assert!(html.contains("<ol>"), "Ordered list not rendered");
assert!(html.contains("<li>Level 1"), "Level 1 not rendered");
assert!(
html.contains("1.1.1.1. Level 4"),
"Deeply nested levels not rendered correctly"
);
}
#[test]
fn test_generate_html_with_raw_html() {
let markdown = r"
# Header with HTML
<p>This is a paragraph with <strong>HTML</strong>.</p>
";
let config = HtmlConfig {
allow_unsafe_html: true,
..HtmlConfig::default()
};
let result = generate_html(markdown, &config);
assert!(result.is_ok());
let html = result.unwrap();
assert!(
html.contains("<p>This is a paragraph with <strong>HTML</strong>.</p>"),
"Raw HTML content not preserved in output"
);
}
#[test]
fn test_generate_html_invalid_front_matter_handling() {
let markdown = "---
key_without_value
another_key: valid
---
# Markdown Content
";
let result = generate_html(markdown, &HtmlConfig::default());
assert!(
result.is_ok(),
"Invalid front matter should not cause an error"
);
let html = result.unwrap();
assert!(
html.contains("<h1>Markdown Content</h1>"),
"Content not processed correctly"
);
}
#[test]
fn test_generate_html_large_front_matter() {
let front_matter = "---\n".to_owned()
+ &"key: value\n".repeat(10_000)
+ "---\n# Content";
let result =
generate_html(&front_matter, &HtmlConfig::default());
assert!(
result.is_ok(),
"Large front matter should be handled gracefully"
);
let html = result.unwrap();
assert!(
html.contains("<h1>Content</h1>"),
"Content not rendered correctly"
);
}
#[test]
fn test_generate_html_with_long_lines() {
let markdown = "A ".repeat(10_000);
let result = markdown_to_html_with_extensions(&markdown);
assert!(result.is_ok());
let html = result.unwrap();
assert!(
html.contains("A A A A"),
"Long consecutive lines should be rendered properly"
);
}
#[test]
fn test_markdown_with_custom_classes() {
let markdown = r":::note
This is a note with a custom class.
:::";
let result = markdown_to_html_with_extensions(markdown);
assert!(result.is_ok(), "Markdown conversion should not fail.");
let html = result.unwrap();
println!("HTML:\n{}", html);
assert!(
html.contains(r#"<div class="note">"#),
"Custom block should wrap in <div class=\"note\">"
);
assert!(
html.contains("This is a note with a custom class."),
"Block text is missing or incorrectly rendered"
);
}
#[test]
fn test_markdown_with_custom_blocks_and_images() {
let markdown = ".class=\"img-fluid\"";
let result = markdown_to_html_with_extensions(markdown);
assert!(result.is_ok());
let html = result.unwrap();
println!("{}", html);
assert!(
html.contains(r#"<img src="https://example.com/image.webp" alt="A very tall building" class="img-fluid" />"#),
"First image not rendered correctly"
);
}
#[test]
fn test_empty_front_matter_handling() {
let markdown = "---\n---\n# Content";
let result = generate_html(markdown, &HtmlConfig::default());
assert!(result.is_ok());
let html = result.unwrap();
assert!(
html.contains("<h1>Content</h1>"),
"Content should be processed correctly"
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn test_invalid_image_syntax() {
let markdown = "![Image with missing URL]()";
let result = process_images_with_classes(markdown);
assert_eq!(
result, markdown,
"Invalid image syntax should remain unchanged"
);
}
#[test]
fn test_incorrect_front_matter_delimiters() {
let markdown = ";;;\ntitle: Test\n---\n# Header";
let result = generate_html(markdown, &HtmlConfig::default());
assert!(result.is_ok());
let html = result.unwrap();
assert!(
html.contains("<h1>Header</h1>"),
"Header should be processed correctly"
);
}
#[cfg(test)]
mod missing_scenarios_tests {
use super::*;
#[test]
fn test_triple_colon_warning_with_bold() {
let markdown = r":::warning
**Caution:** This operation is sensitive.
:::";
let result = markdown_to_html_with_extensions(markdown);
assert!(
result.is_ok(),
"Markdown conversion should succeed."
);
let html = result.unwrap();
println!("HTML:\n{}", html);
assert!(
html.contains(r#"<div class="warning">"#),
"Expected <div class=\"warning\"> wrapping the block"
);
assert!(html.contains("<strong>Caution:</strong>"),
"Expected inline bold text to become <strong>Caution:</strong>");
}
#[test]
fn test_multiple_triple_colon_blocks() {
let markdown = r":::note
**Note:** First block
:::
:::warning
**Warning:** Second block
:::";
let result = markdown_to_html_with_extensions(markdown);
assert!(
result.is_ok(),
"Markdown conversion should succeed."
);
let html = result.unwrap();
println!("HTML:\n{}", html);
assert!(
html.contains(r#"<div class="note">"#),
"Missing <div class=\"note\"> for the first block"
);
assert!(
html.contains(r#"<div class="warning">"#),
"Missing <div class=\"warning\"> for the second block"
);
assert!(
html.contains("<strong>Note:</strong>"),
"Bold text in the note block not parsed"
);
assert!(
html.contains("<strong>Warning:</strong>"),
"Bold text in the warning block not parsed"
);
}
#[test]
fn test_triple_colon_block_multi_paragraph() {
let markdown = r":::note
**Paragraph 1:** This is the first paragraph.
This is the second paragraph, also with **bold** text.
:::";
let result = markdown_to_html_with_extensions(markdown);
assert!(
result.is_ok(),
"Markdown conversion should succeed."
);
let html = result.unwrap();
println!("HTML:\n{}", html);
assert!(
html.contains("<strong>Paragraph 1:</strong>"),
"Inline bold text not parsed in the first paragraph"
);
assert!(html.contains("second paragraph, also with <strong>bold</strong> text"),
"Inline bold text not parsed in the second paragraph");
}
#[test]
fn test_triple_colon_block_forcing_inline_error() {
let markdown = r":::error
This block tries < to break > inline parsing & [some link (unclosed).
:::";
let result = markdown_to_html_with_extensions(markdown);
assert!(
result.is_ok(),
"We won't forcibly error, but let's see the output."
);
let html = result.unwrap();
println!("HTML:\n{}", html);
assert!(
html.contains(r#"<div class="error">"#),
"Block div not found for 'error' class"
);
assert!(
html.contains("This block tries "),
"Expected parsed content in the block"
);
}
}
}