use crate::error::HtmlError;
use crate::extract_front_matter;
use crate::Result;
use mdx_gen::{process_markdown, ComrakOptions, MarkdownOptions};
pub fn generate_html(
markdown: &str,
_config: &crate::HtmlConfig,
) -> Result<String> {
markdown_to_html_with_extensions(markdown)
}
pub fn markdown_to_html_with_extensions(
markdown: &str,
) -> Result<String> {
let content_without_front_matter =
extract_front_matter(markdown).unwrap_or(markdown.to_string());
let mut comrak_options = ComrakOptions::default();
comrak_options.extension.strikethrough = true;
comrak_options.extension.table = true;
comrak_options.extension.autolink = true;
comrak_options.extension.tasklist = true;
comrak_options.extension.superscript = true;
comrak_options.render.unsafe_ = true;
comrak_options.render.escape = false;
let options =
MarkdownOptions::default().with_comrak_options(comrak_options);
match process_markdown(&content_without_front_matter, &options) {
Ok(html_output) => Ok(html_output),
Err(err) => {
Err(HtmlError::markdown_conversion(
err.to_string(),
None, ))
}
}
}
#[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"
);
}
#[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 = ComrakOptions::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) => {
eprintln!("Markdown processing error: {:?}", err);
panic!("Failed to process Markdown with custom ComrakOptions");
}
}
}
#[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("<unexpected>"),
"Raw HTML tags like <unexpected> should not be escaped"
);
assert!(
html.contains("<here>") || html.contains("<here)"),
"Angle brackets in links should be escaped for safety"
);
assert!(
html.contains("<h1>Invalid Markdown <unexpected> [bad](url <here)</h1>"),
"Header not rendered correctly or content not properly handled"
);
}
#[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 result = markdown_to_html_with_extensions(markdown);
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"
);
}
}