#[cfg(test)]
mod tests {
use cmark_writer::ast::{HtmlElement, ListItem, Node};
#[cfg(feature = "gfm")]
use cmark_writer::ast::{TableAlignment, TaskListStatus};
use cmark_writer::writer::{HtmlWriteResult, HtmlWriter, HtmlWriterOptions};
use log::{LevelFilter, Log};
use std::sync::Once;
static INIT: Once = Once::new();
fn setup_logger() {
INIT.call_once(|| {
struct TestLogger;
impl Log for TestLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= LevelFilter::Warn
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
let color_code = match record.level() {
log::Level::Error => "\x1b[31m", log::Level::Warn => "\x1b[33m", log::Level::Info => "\x1b[32m", log::Level::Debug => "\x1b[34m", log::Level::Trace => "\x1b[90m", };
let reset = "\x1b[0m";
println!(
"{}[{}]{} {}: {}",
color_code,
record.level(),
reset,
record.target(),
record.args()
);
}
}
fn flush(&self) {}
}
log::set_boxed_logger(Box::new(TestLogger))
.map(|()| log::set_max_level(LevelFilter::Warn))
.expect("Failed to initialize logger");
});
}
fn render_node_to_html(node: &Node, options: &HtmlWriterOptions) -> HtmlWriteResult<String> {
let mut html_writer = HtmlWriter::with_options(options.clone());
html_writer.write_node(node)?;
let html = html_writer.into_string();
Ok(html)
}
fn render_node_to_html_default(node: &Node) -> HtmlWriteResult<String> {
render_node_to_html(
node,
#[cfg(feature = "gfm")]
&HtmlWriterOptions::default().enable_gfm(true),
#[cfg(not(feature = "gfm"))]
&HtmlWriterOptions::default(),
)
}
#[test]
fn test_paragraph_and_text() {
let node = Node::Paragraph(vec![Node::Text("Hello HTML world!".to_string())]);
let expected_html = "<p>Hello HTML world!</p>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_text_escaping() {
let node = Node::Paragraph(vec![Node::Text("Hello < & > \" ' world!".to_string())]);
let expected_html = "<p>Hello < & > \" ' world!</p>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_heading() {
let node = Node::Heading {
level: 1,
content: vec![Node::Text("Title".to_string())],
heading_type: Default::default(),
};
let expected_html = "<h1>Title</h1>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_emphasis_and_strong() {
let node = Node::Paragraph(vec![
Node::Text("This is ".to_string()),
Node::Emphasis(vec![Node::Text("emphasized".to_string())]),
Node::Text(" and this is ".to_string()),
Node::Strong(vec![Node::Text("strong".to_string())]),
Node::Text("!".to_string()),
]);
let expected_html =
"<p>This is <em>emphasized</em> and this is <strong>strong</strong>!</p>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_thematic_break() {
let node = Node::ThematicBreak;
let expected_html = "<hr />\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_inline_code() {
let node = Node::InlineCode("let x = 1;".to_string());
let expected_html = "<code>let x = 1;</code>";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_code_block_default_options() {
let node = Node::CodeBlock {
language: Some("rust".to_string()),
content: "fn main() {\n println!(\"Hello\");\n}".to_string(),
block_type: Default::default(),
};
let expected_html = "<pre><code class=\"language-rust\">fn main() {\n println!(\"Hello\");\n}</code></pre>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_code_block_custom_options() {
let node = Node::CodeBlock {
language: Some("python".to_string()),
content: "print(\"Hello\")".to_string(),
block_type: Default::default(),
};
#[cfg(feature = "gfm")]
let options = HtmlWriterOptions {
code_block_language_class_prefix: Some("lang-".to_string()),
strict: false,
..Default::default()
};
#[cfg(not(feature = "gfm"))]
let options = HtmlWriterOptions {
code_block_language_class_prefix: Some("lang-".to_string()),
strict: false,
};
let expected_html = "<pre><code class=\"lang-python\">print(\"Hello\")</code></pre>\n";
assert_eq!(render_node_to_html(&node, &options).unwrap(), expected_html);
}
#[test]
fn test_code_block_no_prefix_option() {
let node = Node::CodeBlock {
language: Some("rust".to_string()),
content: "let _ = 1;".to_string(),
block_type: Default::default(),
};
#[cfg(feature = "gfm")]
let options = HtmlWriterOptions {
code_block_language_class_prefix: None,
strict: false,
..Default::default()
};
#[cfg(not(feature = "gfm"))]
let options = HtmlWriterOptions {
code_block_language_class_prefix: None,
strict: false,
};
let expected_html = "<pre><code>let _ = 1;</code></pre>\n";
assert_eq!(render_node_to_html(&node, &options).unwrap(), expected_html);
}
#[test]
fn test_code_block_no_language() {
let node = Node::CodeBlock {
language: None,
content: "plain text".to_string(),
block_type: Default::default(),
};
let expected_html = "<pre><code>plain text</code></pre>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_link() {
let node = Node::Link {
url: "https://example.com".to_string(),
title: Some("Example Domain".to_string()),
content: vec![Node::Text("Visit Example".to_string())],
};
let expected_html =
"<a href=\"https://example.com\" title=\"Example Domain\">Visit Example</a>";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_image() {
let node = Node::Image {
url: "/logo.png".to_string(),
title: Some("Logo".to_string()),
alt: vec![Node::Text("Site Logo".to_string())],
};
let expected_html = "<img src=\"/logo.png\" alt=\"Site Logo\" title=\"Logo\" />";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_unordered_list() {
let node = Node::UnorderedList(vec![
ListItem::Unordered {
content: vec![Node::Paragraph(vec![Node::Text("Item 1".to_string())])],
},
ListItem::Unordered {
content: vec![Node::Paragraph(vec![Node::Text("Item 2".to_string())])],
},
]);
let expected_html = "<ul>\n<li><p>Item 1</p>\n</li>\n<li><p>Item 2</p>\n</li>\n</ul>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_ordered_list() {
let node = Node::OrderedList {
start: 3,
items: vec![
ListItem::Ordered {
number: None,
content: vec![Node::Paragraph(vec![Node::Text("Item A".to_string())])],
},
ListItem::Ordered {
number: Some(5),
content: vec![Node::Paragraph(vec![Node::Text("Item B".to_string())])],
},
],
};
let expected_html =
"<ol start=\"3\">\n<li><p>Item A</p>\n</li>\n<li><p>Item B</p>\n</li>\n</ol>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_html_block() {
let node = Node::HtmlBlock("<div class=\"foo\">Bar</div>".to_string());
let expected_html = "<div class=\"foo\">Bar</div>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_html_element() {
let element = HtmlElement {
tag: "my-custom-tag".to_string(),
attributes: vec![cmark_writer::ast::HtmlAttribute {
name: "data-val".to_string(),
value: "xyz".to_string(),
}],
children: vec![Node::Text("Content".to_string())],
self_closing: false,
};
let node = Node::HtmlElement(element);
let expected_html = "<my-custom-tag data-val=\"xyz\">Content</my-custom-tag>";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_self_closing_html_element() {
let element = HtmlElement {
tag: "hr".to_string(),
attributes: vec![cmark_writer::ast::HtmlAttribute {
name: "class".to_string(),
value: "fancy".to_string(),
}],
children: vec![],
self_closing: true,
};
let node = Node::HtmlElement(element);
let expected_html = "<hr class=\"fancy\" />";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[cfg(feature = "gfm")]
#[test]
fn test_strikethrough_gfm() {
let node = Node::Strikethrough(vec![Node::Text("deleted".to_string())]);
let expected_html = "<del>deleted</del>";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[cfg(feature = "gfm")]
#[test]
fn test_task_list_item_gfm() {
let unchecked_item = ListItem::Task {
status: TaskListStatus::Unchecked,
content: vec![Node::Text("To do".to_string())],
};
let checked_item = ListItem::Task {
status: TaskListStatus::Checked,
content: vec![Node::Text("Done".to_string())],
};
let node = Node::UnorderedList(vec![unchecked_item, checked_item]);
let options = HtmlWriterOptions {
enable_gfm: true,
..HtmlWriterOptions::default()
};
let expected_html = "<ul>\n<li class=\"task-list-item\"><input type=\"checkbox\" disabled=\"\" /> To do</li>\n<li class=\"task-list-item task-list-item-checked\"><input type=\"checkbox\" disabled=\"\" checked=\"\" /> Done</li>\n</ul>\n";
assert_eq!(render_node_to_html(&node, &options).unwrap(), expected_html);
}
#[test]
fn test_blockquote() {
let node = Node::BlockQuote(vec![
Node::Paragraph(vec![Node::Text("This is a quote.".to_string())]),
Node::Paragraph(vec![Node::Text("Another paragraph in quote.".to_string())]),
]);
let expected_html =
"<blockquote>\n<p>This is a quote.</p>\n<p>Another paragraph in quote.</p>\n</blockquote>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_autolink_uri() {
let node = Node::Autolink {
url: "https://example.com".to_string(),
is_email: false,
};
let expected_html = "<a href=\"https://example.com\">https://example.com</a>";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_autolink_email() {
let node = Node::Autolink {
url: "test@example.com".to_string(),
is_email: true,
};
let expected_html = "<a href=\"mailto:test@example.com\">test@example.com</a>";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
#[cfg(feature = "gfm")]
fn test_extended_autolink() {
let node = Node::ExtendedAutolink("www.example.com/path".to_string());
let expected_html = "<a href=\"www.example.com/path\">www.example.com/path</a>";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_reference_link_full() {
let node = Node::ReferenceLink {
label: "lbl".to_string(),
content: vec![Node::Text("link text".to_string())],
};
let options = HtmlWriterOptions {
strict: false,
..HtmlWriterOptions::default()
};
let expected_html = "[link text][lbl]";
assert_eq!(render_node_to_html(&node, &options).unwrap(), expected_html);
}
#[test]
fn test_reference_link_shortcut() {
let node = Node::ReferenceLink {
label: "shortcut".to_string(),
content: vec![], };
let options = HtmlWriterOptions {
strict: false,
..HtmlWriterOptions::default()
};
let expected_html = "[shortcut]";
assert_eq!(render_node_to_html(&node, &options).unwrap(), expected_html);
}
#[test]
fn test_table_basic() {
let node = Node::Table {
headers: vec![
Node::Text("Header 1".to_string()),
Node::Text("Header 2".to_string()),
],
#[cfg(feature = "gfm")]
alignments: vec![], rows: vec![
vec![
Node::Text("Cell 1.1".to_string()),
Node::Text("Cell 1.2".to_string()),
],
vec![
Node::Text("Cell 2.1".to_string()),
Node::Text("Cell 2.2".to_string()),
],
],
};
let expected_html = "<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Cell 1.1</td>\n<td>Cell 1.2</td>\n</tr>\n<tr>\n<td>Cell 2.1</td>\n<td>Cell 2.2</td>\n</tr>\n</tbody>\n</table>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[cfg(feature = "gfm")]
#[test]
fn test_table_with_gfm_alignment() {
let node = Node::Table {
headers: vec![
Node::Text("H1".to_string()),
Node::Text("H2".to_string()),
Node::Text("H3".to_string()),
],
alignments: vec![
TableAlignment::Left,
TableAlignment::Center,
TableAlignment::Right,
],
rows: vec![vec![
Node::Text("L".to_string()),
Node::Text("C".to_string()),
Node::Text("R".to_string()),
]],
};
let expected_html = "<table>\n<thead>\n<tr>\n<th style=\"text-align: left;\">H1</th>\n<th style=\"text-align: center;\">H2</th>\n<th style=\"text-align: right;\">H3</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align: left;\">L</td>\n<td style=\"text-align: center;\">C</td>\n<td style=\"text-align: right;\">R</td>\n</tr>\n</tbody>\n</table>\n";
assert_eq!(render_node_to_html_default(&node).unwrap(), expected_html);
}
#[test]
fn test_warning_output() {
setup_logger();
let element = HtmlElement {
tag: "invalid<tag>".to_string(),
attributes: vec![],
children: vec![Node::Text("Content".to_string())],
self_closing: false,
};
let node = Node::HtmlElement(element);
let options = HtmlWriterOptions {
strict: false,
..HtmlWriterOptions::default()
};
let expected_html = "<invalid<tag>>Content</invalid<tag>>";
let actual_html = render_node_to_html(&node, &options).unwrap();
assert_eq!(actual_html, expected_html);
}
#[test]
fn test_invalid_html_attribute_non_strict() {
let element = HtmlElement {
tag: "div".to_string(),
attributes: vec![cmark_writer::ast::HtmlAttribute {
name: "invalid<attr>".to_string(),
value: "value".to_string(),
}],
children: vec![Node::Text("Content".to_string())],
self_closing: false,
};
let node = Node::HtmlElement(element);
let options = HtmlWriterOptions {
strict: false,
..HtmlWriterOptions::default()
};
let expected_html = "<div invalid<attr>=\"value\">Content</div>";
assert_eq!(render_node_to_html(&node, &options).unwrap(), expected_html);
}
#[cfg(feature = "gfm")]
#[test]
fn test_disallowed_html_tag_gfm() {
let element = HtmlElement {
tag: "script".to_string(),
attributes: vec![],
children: vec![Node::Text("alert('test')".to_string())],
self_closing: false,
};
let node = Node::HtmlElement(element);
let options = HtmlWriterOptions {
enable_gfm: true,
..HtmlWriterOptions::default()
};
let expected_html = "<script>alert('test')</script>";
assert_eq!(render_node_to_html(&node, &options).unwrap(), expected_html);
}
#[test]
fn test_reference_link_warning() {
let node = Node::ReferenceLink {
label: "unresolved".to_string(),
content: vec![Node::Text("Unresolved Link".to_string())],
};
let options = HtmlWriterOptions {
strict: false,
..HtmlWriterOptions::default()
};
let expected_html = "[Unresolved Link][unresolved]";
assert_eq!(render_node_to_html(&node, &options).unwrap(), expected_html);
}
#[cfg(feature = "gfm")]
#[test]
fn test_gfm_warning_output() {
setup_logger();
let element = HtmlElement {
tag: "script".to_string(),
attributes: vec![],
children: vec![Node::Text("alert('test')".to_string())],
self_closing: false,
};
let node = Node::HtmlElement(element);
let options = HtmlWriterOptions {
enable_gfm: true,
gfm_disallowed_html_tags: vec!["script".to_string()],
..HtmlWriterOptions::default()
};
let expected_html = "<script>alert('test')</script>";
let actual_html = render_node_to_html(&node, &options).unwrap();
assert_eq!(actual_html, expected_html);
}
}