#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockQuoteKind {
Note,
Tip,
Important,
Warning,
Caution,
}
impl From<pulldown_cmark::BlockQuoteKind> for BlockQuoteKind {
fn from(kind: pulldown_cmark::BlockQuoteKind) -> Self {
match kind {
pulldown_cmark::BlockQuoteKind::Note => BlockQuoteKind::Note,
pulldown_cmark::BlockQuoteKind::Tip => BlockQuoteKind::Tip,
pulldown_cmark::BlockQuoteKind::Important => BlockQuoteKind::Important,
pulldown_cmark::BlockQuoteKind::Warning => BlockQuoteKind::Warning,
pulldown_cmark::BlockQuoteKind::Caution => BlockQuoteKind::Caution,
}
}
}
impl From<&pulldown_cmark::BlockQuoteKind> for BlockQuoteKind {
fn from(kind: &pulldown_cmark::BlockQuoteKind) -> Self {
(*kind).into()
}
}
impl BlockQuoteKind {
pub fn as_str(&self) -> &'static str {
match self {
BlockQuoteKind::Note => "note",
BlockQuoteKind::Tip => "tip",
BlockQuoteKind::Important => "important",
BlockQuoteKind::Warning => "warning",
BlockQuoteKind::Caution => "caution",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkType {
Inline,
Reference,
ReferenceUnknown,
Collapsed,
CollapsedUnknown,
Shortcut,
ShortcutUnknown,
Autolink,
Email,
WikiLink(bool),
}
impl From<pulldown_cmark::LinkType> for LinkType {
fn from(link_type: pulldown_cmark::LinkType) -> Self {
match link_type {
pulldown_cmark::LinkType::Inline => LinkType::Inline,
pulldown_cmark::LinkType::Reference => LinkType::Reference,
pulldown_cmark::LinkType::ReferenceUnknown => LinkType::ReferenceUnknown,
pulldown_cmark::LinkType::Collapsed => LinkType::Collapsed,
pulldown_cmark::LinkType::CollapsedUnknown => LinkType::CollapsedUnknown,
pulldown_cmark::LinkType::Shortcut => LinkType::Shortcut,
pulldown_cmark::LinkType::ShortcutUnknown => LinkType::ShortcutUnknown,
pulldown_cmark::LinkType::Autolink => LinkType::Autolink,
pulldown_cmark::LinkType::Email => LinkType::Email,
pulldown_cmark::LinkType::WikiLink { has_pothole } => LinkType::WikiLink(has_pothole),
}
}
}
impl From<&pulldown_cmark::LinkType> for LinkType {
fn from(link_type: &pulldown_cmark::LinkType) -> Self {
(*link_type).into()
}
}
impl LinkType {
pub fn as_str(&self) -> &'static str {
match self {
LinkType::Inline => "inline",
LinkType::Reference => "reference",
LinkType::ReferenceUnknown => "reference_unknown",
LinkType::Collapsed => "collapsed",
LinkType::CollapsedUnknown => "collapsed_unknown",
LinkType::Shortcut => "shortcut",
LinkType::ShortcutUnknown => "shortcut_unknown",
LinkType::Autolink => "autolink",
LinkType::Email => "email",
LinkType::WikiLink(_) => "wikilink",
}
}
}
pub trait HeadingComponent {
fn render_start(&self, level: u8, id: Option<&str>, classes: &[&str]) -> String {
let class_attr = if !classes.is_empty() {
format!(" class=\"{}\"", classes.join(" "))
} else {
String::new()
};
let id_attr = id
.as_ref()
.map(|i| format!(" id=\"{}\"", i))
.unwrap_or_default();
format!("<h{}{}{}>", level, id_attr, class_attr)
}
fn render_end(&self, level: u8) -> String {
format!("</h{}>", level)
}
}
pub trait ParagraphComponent {
fn render_start(&self) -> String {
"<p>".to_string()
}
fn render_end(&self) -> String {
"</p>".to_string()
}
}
pub trait LinkComponent {
fn render_start(&self, url: &str, title: Option<&str>, link_type: LinkType) -> String;
fn render_end(&self) -> String {
"</a>".to_string()
}
}
pub trait ImageComponent {
fn render(&self, url: &str, alt: &str, title: Option<&str>) -> String;
}
pub trait StrongComponent {
fn render_start(&self) -> String {
"<strong>".to_string()
}
fn render_end(&self) -> String {
"</strong>".to_string()
}
}
pub trait EmphasisComponent {
fn render_start(&self) -> String {
"<em>".to_string()
}
fn render_end(&self) -> String {
"</em>".to_string()
}
}
pub trait CodeComponent {
fn render(&self, code: &str) -> String;
}
pub trait BlockquoteComponent {
fn render_start(&self, kind: Option<BlockQuoteKind>) -> String {
match kind {
Some(k) => format!("<blockquote data-kind=\"{}\">", k.as_str()),
None => "<blockquote>".to_string(),
}
}
fn render_end(&self, _kind: Option<BlockQuoteKind>) -> String {
"</blockquote>".to_string()
}
}
pub trait HardBreakComponent {
fn render(&self) -> String {
"<br />".to_string()
}
}
pub trait HorizontalRuleComponent {
fn render(&self) -> String {
"<hr />".to_string()
}
}
pub trait ListComponent {
fn render_start(&self, list_type: ListType, start_number: Option<u64>) -> String {
match list_type {
ListType::Ordered => {
if let Some(start) = start_number {
if start != 1 {
format!("<ol start=\"{}\">", start)
} else {
"<ol>".to_string()
}
} else {
"<ol>".to_string()
}
}
ListType::Unordered => "<ul>".to_string(),
}
}
fn render_end(&self, list_type: ListType) -> String {
match list_type {
ListType::Ordered => "</ol>".to_string(),
ListType::Unordered => "</ul>".to_string(),
}
}
}
pub trait ListItemComponent {
fn render_start(&self) -> String {
"<li>".to_string()
}
fn render_end(&self) -> String {
"</li>".to_string()
}
}
pub trait StrikethroughComponent {
fn render_start(&self) -> String {
"<del>".to_string()
}
fn render_end(&self) -> String {
"</del>".to_string()
}
}
pub trait TaskListMarkerComponent {
fn render(&self, checked: bool) -> String {
if checked {
"<input type=\"checkbox\" checked disabled />".to_string()
} else {
"<input type=\"checkbox\" disabled />".to_string()
}
}
}
pub trait TableComponent {
fn render_start(&self, _column_alignments: &[TableAlignment]) -> String {
"<table>".to_string()
}
fn render_end(&self) -> String {
"</table>".to_string()
}
}
pub trait TableHeadComponent {
fn render_start(&self) -> String {
"<thead>".to_string()
}
fn render_end(&self) -> String {
"</thead>".to_string()
}
}
pub trait TableRowComponent {
fn render_start(&self) -> String {
"<tr>".to_string()
}
fn render_end(&self) -> String {
"</tr>".to_string()
}
}
pub trait TableCellComponent {
fn render_start(&self, is_header: bool, alignment: Option<TableAlignment>) -> String {
let tag = if is_header { "th" } else { "td" };
match alignment {
Some(TableAlignment::Left) => format!("<{} style=\"text-align: left\">", tag),
Some(TableAlignment::Center) => format!("<{} style=\"text-align: center\">", tag),
Some(TableAlignment::Right) => format!("<{} style=\"text-align: right\">", tag),
None => format!("<{}>", tag),
}
}
fn render_end(&self, is_header: bool) -> String {
if is_header {
"</th>".to_string()
} else {
"</td>".to_string()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListType {
Ordered,
Unordered,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TableAlignment {
Left,
Center,
Right,
}
#[derive(Default)]
pub struct MarkdownComponents {
pub heading: Option<Box<dyn HeadingComponent + Send + Sync>>,
pub paragraph: Option<Box<dyn ParagraphComponent + Send + Sync>>,
pub link: Option<Box<dyn LinkComponent + Send + Sync>>,
pub image: Option<Box<dyn ImageComponent + Send + Sync>>,
pub strong: Option<Box<dyn StrongComponent + Send + Sync>>,
pub emphasis: Option<Box<dyn EmphasisComponent + Send + Sync>>,
pub code: Option<Box<dyn CodeComponent + Send + Sync>>,
pub blockquote: Option<Box<dyn BlockquoteComponent + Send + Sync>>,
pub hard_break: Option<Box<dyn HardBreakComponent + Send + Sync>>,
pub horizontal_rule: Option<Box<dyn HorizontalRuleComponent + Send + Sync>>,
pub list: Option<Box<dyn ListComponent + Send + Sync>>,
pub list_item: Option<Box<dyn ListItemComponent + Send + Sync>>,
pub strikethrough: Option<Box<dyn StrikethroughComponent + Send + Sync>>,
pub task_list_marker: Option<Box<dyn TaskListMarkerComponent + Send + Sync>>,
pub table: Option<Box<dyn TableComponent + Send + Sync>>,
pub table_head: Option<Box<dyn TableHeadComponent + Send + Sync>>,
pub table_row: Option<Box<dyn TableRowComponent + Send + Sync>>,
pub table_cell: Option<Box<dyn TableCellComponent + Send + Sync>>,
}
impl MarkdownComponents {
pub fn new() -> Self {
Self::default()
}
pub fn has_any_components(&self) -> bool {
self.heading.is_some()
|| self.paragraph.is_some()
|| self.link.is_some()
|| self.image.is_some()
|| self.strong.is_some()
|| self.emphasis.is_some()
|| self.code.is_some()
|| self.blockquote.is_some()
|| self.hard_break.is_some()
|| self.horizontal_rule.is_some()
|| self.list.is_some()
|| self.list_item.is_some()
|| self.strikethrough.is_some()
|| self.task_list_marker.is_some()
|| self.table.is_some()
|| self.table_head.is_some()
|| self.table_row.is_some()
|| self.table_cell.is_some()
}
pub fn heading<C: HeadingComponent + Send + Sync + 'static>(mut self, component: C) -> Self {
self.heading = Some(Box::new(component));
self
}
pub fn paragraph<C: ParagraphComponent + Send + Sync + 'static>(
mut self,
component: C,
) -> Self {
self.paragraph = Some(Box::new(component));
self
}
pub fn link<C: LinkComponent + Send + Sync + 'static>(mut self, component: C) -> Self {
self.link = Some(Box::new(component));
self
}
pub fn image<C: ImageComponent + Send + Sync + 'static>(mut self, component: C) -> Self {
self.image = Some(Box::new(component));
self
}
pub fn strong<C: StrongComponent + Send + Sync + 'static>(mut self, component: C) -> Self {
self.strong = Some(Box::new(component));
self
}
pub fn emphasis<C: EmphasisComponent + Send + Sync + 'static>(mut self, component: C) -> Self {
self.emphasis = Some(Box::new(component));
self
}
pub fn code<C: CodeComponent + Send + Sync + 'static>(mut self, component: C) -> Self {
self.code = Some(Box::new(component));
self
}
pub fn blockquote<C: BlockquoteComponent + Send + Sync + 'static>(
mut self,
component: C,
) -> Self {
self.blockquote = Some(Box::new(component));
self
}
pub fn hard_break<C: HardBreakComponent + Send + Sync + 'static>(
mut self,
component: C,
) -> Self {
self.hard_break = Some(Box::new(component));
self
}
pub fn horizontal_rule<C: HorizontalRuleComponent + Send + Sync + 'static>(
mut self,
component: C,
) -> Self {
self.horizontal_rule = Some(Box::new(component));
self
}
pub fn list<C: ListComponent + Send + Sync + 'static>(mut self, component: C) -> Self {
self.list = Some(Box::new(component));
self
}
pub fn list_item<C: ListItemComponent + Send + Sync + 'static>(mut self, component: C) -> Self {
self.list_item = Some(Box::new(component));
self
}
pub fn strikethrough<C: StrikethroughComponent + Send + Sync + 'static>(
mut self,
component: C,
) -> Self {
self.strikethrough = Some(Box::new(component));
self
}
pub fn task_list_marker<C: TaskListMarkerComponent + Send + Sync + 'static>(
mut self,
component: C,
) -> Self {
self.task_list_marker = Some(Box::new(component));
self
}
pub fn table<C: TableComponent + Send + Sync + 'static>(mut self, component: C) -> Self {
self.table = Some(Box::new(component));
self
}
pub fn table_head<C: TableHeadComponent + Send + Sync + 'static>(
mut self,
component: C,
) -> Self {
self.table_head = Some(Box::new(component));
self
}
pub fn table_row<C: TableRowComponent + Send + Sync + 'static>(mut self, component: C) -> Self {
self.table_row = Some(Box::new(component));
self
}
pub fn table_cell<C: TableCellComponent + Send + Sync + 'static>(
mut self,
component: C,
) -> Self {
self.table_cell = Some(Box::new(component));
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::content::{MarkdownOptions, render_markdown};
struct TestCustomHeading;
impl HeadingComponent for TestCustomHeading {
fn render_start(&self, level: u8, id: Option<&str>, classes: &[&str]) -> String {
let id_attr = id.map(|i| format!(" id=\"{}\"", i)).unwrap_or_default();
let class_attr = if classes.is_empty() {
String::new()
} else {
format!(" class=\"{}\"", classes.join(" "))
};
format!("<h{}{}{}>🎯", level, id_attr, class_attr)
}
fn render_end(&self, level: u8) -> String {
format!("</h{}>", level)
}
}
struct TestCustomParagraph;
impl ParagraphComponent for TestCustomParagraph {
fn render_start(&self) -> String {
"<p class=\"custom-paragraph\">".to_string()
}
fn render_end(&self) -> String {
"</p><!-- end custom paragraph -->".to_string()
}
}
struct TestCustomLink;
impl LinkComponent for TestCustomLink {
fn render_start(&self, url: &str, title: Option<&str>, _link_type: LinkType) -> String {
let title_attr = title
.map(|t| format!(" title=\"{}\"", t))
.unwrap_or_default();
format!("<a href=\"{}\" class=\"custom-link\"{}>🔗", url, title_attr)
}
fn render_end(&self) -> String {
"</a>".to_string()
}
}
struct TestCustomImage;
impl ImageComponent for TestCustomImage {
fn render(&self, url: &str, alt: &str, title: Option<&str>) -> String {
let title_attr = title
.map(|t| format!(" title=\"{}\"", t))
.unwrap_or_default();
format!(
"<img src=\"{}\" alt=\"{}\" class=\"custom-image\"{} />📸",
url, alt, title_attr
)
}
}
struct TestCustomStrong;
impl StrongComponent for TestCustomStrong {
fn render_start(&self) -> String {
"<strong class=\"custom-strong\">💪".to_string()
}
fn render_end(&self) -> String {
"</strong>".to_string()
}
}
struct TestCustomEmphasis;
impl EmphasisComponent for TestCustomEmphasis {
fn render_start(&self) -> String {
"<em class=\"custom-emphasis\">✨".to_string()
}
fn render_end(&self) -> String {
"</em>".to_string()
}
}
struct TestCustomCode;
impl CodeComponent for TestCustomCode {
fn render(&self, code: &str) -> String {
format!("<code class=\"custom-code\">💻{}</code>", code)
}
}
struct TestCustomBlockquote;
impl BlockquoteComponent for TestCustomBlockquote {
fn render_start(&self, kind: Option<BlockQuoteKind>) -> String {
match kind {
Some(k) => format!(
"<blockquote class=\"custom-blockquote {}\" data-kind=\"{}\">📝",
k.as_str(),
k.as_str()
),
None => "<blockquote class=\"custom-blockquote\">📝".to_string(),
}
}
fn render_end(&self, _kind: Option<BlockQuoteKind>) -> String {
"</blockquote>".to_string()
}
}
#[test]
fn test_components_builder_pattern() {
let components = MarkdownComponents::new().heading(TestCustomHeading);
assert!(components.heading.is_some());
assert!(components.paragraph.is_none());
assert!(components.link.is_none());
}
#[test]
fn test_has_any_components() {
let empty_components = MarkdownComponents::new();
assert!(!empty_components.has_any_components());
let with_heading = MarkdownComponents::new().heading(TestCustomHeading);
assert!(with_heading.has_any_components());
}
#[test]
fn test_custom_heading_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().heading(TestCustomHeading),
..Default::default()
};
let html = render_markdown("# Hello, world!", Some(&options), None, None);
assert!(html.contains("🎯Hello, world!"));
}
#[test]
fn test_custom_paragraph_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().paragraph(TestCustomParagraph),
..Default::default()
};
let content = render_markdown("This is a paragraph.", Some(&options), None, None);
assert!(content.contains(
"<p class=\"custom-paragraph\">This is a paragraph.</p><!-- end custom paragraph -->"
));
}
#[test]
fn test_custom_link_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().link(TestCustomLink),
..Default::default()
};
let content = render_markdown("[Example](https://example.com)", Some(&options), None, None);
assert!(
content.contains("<a href=\"https://example.com\" class=\"custom-link\">🔗Example</a>")
);
}
#[test]
fn test_custom_image_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().image(TestCustomImage),
..Default::default()
};
let content = render_markdown("", Some(&options), None, None);
assert!(
content.contains("<img src=\"image.jpg\" alt=\"Alt text\" class=\"custom-image\" />📸")
);
}
#[test]
fn test_custom_strong_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().strong(TestCustomStrong),
..Default::default()
};
let content = render_markdown("**Bold text**", Some(&options), None, None);
assert!(content.contains("<strong class=\"custom-strong\">💪Bold text</strong>"));
}
#[test]
fn test_custom_emphasis_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().emphasis(TestCustomEmphasis),
..Default::default()
};
let content = render_markdown("*Italic text*", Some(&options), None, None);
assert!(content.contains("<em class=\"custom-emphasis\">✨Italic text</em>"));
}
#[test]
fn test_custom_code_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().code(TestCustomCode),
..Default::default()
};
let content = render_markdown("`console.log('hello')`", Some(&options), None, None);
assert!(content.contains("<code class=\"custom-code\">💻console.log('hello')</code>"));
}
#[test]
fn test_custom_blockquote_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().blockquote(TestCustomBlockquote),
..Default::default()
};
let content = render_markdown("> This is a quote", Some(&options), None, None);
assert!(content.contains("<blockquote class=\"custom-blockquote\">📝"));
assert!(content.contains("</blockquote>"));
assert!(content.contains("This is a quote"));
}
#[test]
fn test_multiple_custom_components() {
let options = MarkdownOptions {
components: MarkdownComponents::new()
.heading(TestCustomHeading)
.paragraph(TestCustomParagraph)
.link(TestCustomLink)
.strong(TestCustomStrong),
..Default::default()
};
let content = render_markdown(
"# Title\n\nThis is a **bold** [link](https://example.com).",
Some(&options),
None,
None,
);
assert!(content.contains("🎯Title"));
assert!(content.contains("<p class=\"custom-paragraph\">"));
assert!(content.contains("<strong class=\"custom-strong\">💪bold</strong>"));
assert!(
content.contains("<a href=\"https://example.com\" class=\"custom-link\">🔗link</a>")
);
}
#[test]
fn test_nested_components() {
let options = MarkdownOptions {
components: MarkdownComponents::new()
.blockquote(TestCustomBlockquote)
.strong(TestCustomStrong)
.emphasis(TestCustomEmphasis)
.code(TestCustomCode),
..Default::default()
};
let content = render_markdown(
"> This is a **bold** and *italic* with `code`",
Some(&options),
None,
None,
);
assert!(content.contains("<blockquote class=\"custom-blockquote\">📝"));
assert!(content.contains("<strong class=\"custom-strong\">💪bold</strong>"));
assert!(content.contains("<em class=\"custom-emphasis\">✨italic</em>"));
assert!(content.contains("<code class=\"custom-code\">💻code</code>"));
assert!(content.contains("</blockquote>"));
}
struct TestHardBreak;
impl HardBreakComponent for TestHardBreak {
fn render(&self) -> String {
"<br class=\"custom-break\" />".to_string()
}
}
struct TestHorizontalRule;
impl HorizontalRuleComponent for TestHorizontalRule {
fn render(&self) -> String {
"<hr class=\"custom-rule\" />".to_string()
}
}
struct TestList;
impl ListComponent for TestList {
fn render_start(&self, list_type: ListType, start_number: Option<u64>) -> String {
match list_type {
ListType::Ordered => format!(
"<ol class=\"custom-list\" start=\"{}\">",
start_number.unwrap_or(1)
),
ListType::Unordered => "<ul class=\"custom-list\">".to_string(),
}
}
fn render_end(&self, list_type: ListType) -> String {
match list_type {
ListType::Ordered => "</ol>".to_string(),
ListType::Unordered => "</ul>".to_string(),
}
}
}
struct TestListItem;
impl ListItemComponent for TestListItem {
fn render_start(&self) -> String {
"<li class=\"custom-item\">".to_string()
}
fn render_end(&self) -> String {
"</li>".to_string()
}
}
struct TestStrikethrough;
impl StrikethroughComponent for TestStrikethrough {
fn render_start(&self) -> String {
"<del class=\"custom-strike\">".to_string()
}
fn render_end(&self) -> String {
"</del>".to_string()
}
}
struct TestTaskListMarker;
impl TaskListMarkerComponent for TestTaskListMarker {
fn render(&self, checked: bool) -> String {
if checked {
"<input type=\"checkbox\" checked class=\"custom-task\" />"
} else {
"<input type=\"checkbox\" class=\"custom-task\" />"
}
.to_string()
}
}
struct TestTable;
impl TableComponent for TestTable {
fn render_start(&self, _alignments: &[TableAlignment]) -> String {
"<table class=\"custom-table\">".to_string()
}
fn render_end(&self) -> String {
"</table>".to_string()
}
}
struct TestTableHead;
impl TableHeadComponent for TestTableHead {
fn render_start(&self) -> String {
"<thead class=\"custom-thead\">".to_string()
}
fn render_end(&self) -> String {
"</thead>".to_string()
}
}
struct TestTableRow;
impl TableRowComponent for TestTableRow {
fn render_start(&self) -> String {
"<tr class=\"custom-row\">".to_string()
}
fn render_end(&self) -> String {
"</tr>".to_string()
}
}
struct TestTableCell;
impl TableCellComponent for TestTableCell {
fn render_start(&self, is_header: bool, alignment: Option<TableAlignment>) -> String {
let tag = if is_header { "th" } else { "td" };
let align = match alignment {
Some(TableAlignment::Left) => " style=\"text-align: left\"",
Some(TableAlignment::Center) => " style=\"text-align: center\"",
Some(TableAlignment::Right) => " style=\"text-align: right\"",
None => "",
};
format!("<{} class=\"custom-cell\"{}>", tag, align)
}
fn render_end(&self, is_header: bool) -> String {
if is_header {
"</th>".to_string()
} else {
"</td>".to_string()
}
}
}
#[test]
fn test_hard_break_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().hard_break(TestHardBreak),
..Default::default()
};
let content = render_markdown("Line 1 \nLine 2", Some(&options), None, None);
assert!(content.contains("<br class=\"custom-break\" />"));
}
#[test]
fn test_horizontal_rule_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().horizontal_rule(TestHorizontalRule),
..Default::default()
};
let content = render_markdown("---", Some(&options), None, None);
assert!(content.contains("<hr class=\"custom-rule\" />"));
}
#[test]
fn test_list_components() {
let options = MarkdownOptions {
components: MarkdownComponents::new()
.list(TestList)
.list_item(TestListItem),
..Default::default()
};
let content = render_markdown(
"1. First\n2. Second\n\n- Bullet\n- Point",
Some(&options),
None,
None,
);
assert!(content.contains("<ol class=\"custom-list\" start=\"1\">"));
assert!(content.contains("<ul class=\"custom-list\">"));
assert!(content.contains("<li class=\"custom-item\">"));
}
#[test]
fn test_strikethrough_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().strikethrough(TestStrikethrough),
..Default::default()
};
let content = render_markdown("~~strikethrough~~", Some(&options), None, None);
assert!(content.contains("<del class=\"custom-strike\">"));
}
#[test]
fn test_task_list_component() {
let options = MarkdownOptions {
components: MarkdownComponents::new().task_list_marker(TestTaskListMarker),
..Default::default()
};
let content = render_markdown("- [x] Done\n- [ ] Todo", Some(&options), None, None);
assert!(content.contains("<input type=\"checkbox\" checked class=\"custom-task\" />"));
assert!(content.contains("<input type=\"checkbox\" class=\"custom-task\" />"));
}
#[test]
fn test_table_components() {
let options = MarkdownOptions {
components: MarkdownComponents::new()
.table(TestTable)
.table_head(TestTableHead)
.table_row(TestTableRow)
.table_cell(TestTableCell),
..Default::default()
};
let content = render_markdown(
"| Header | Header |\n|--------|--------|\n| Cell | Cell |",
Some(&options),
None,
None,
);
assert!(content.contains("<table class=\"custom-table\">"));
assert!(content.contains("<thead class=\"custom-thead\">"));
assert!(content.contains("<tr class=\"custom-row\">"));
assert!(content.contains("<td class=\"custom-cell\">"));
}
}