use bun_md::root::{self, Options};
#[test]
fn render_heading() {
let html = root::render_to_html(b"# Hello").unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<h1>"), "got: {s}");
assert!(s.contains("Hello"), "got: {s}");
}
#[test]
fn render_paragraph() {
let html = root::render_to_html(b"Hello world").unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<p>Hello world</p>"), "got: {s}");
}
#[test]
fn render_bold_and_italic() {
let html = root::render_to_html(b"**bold** and *italic*").unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<strong>bold</strong>"), "got: {s}");
assert!(s.contains("<em>italic</em>"), "got: {s}");
}
#[test]
fn render_code_block() {
let input = b"```rust\nfn main() {}\n```";
let html = root::render_to_html(input).unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<code"), "got: {s}");
assert!(s.contains("fn main()"), "got: {s}");
}
#[test]
fn render_inline_code() {
let html = root::render_to_html(b"use `cargo test`").unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<code>cargo test</code>"), "got: {s}");
}
#[test]
fn render_link() {
let html = root::render_to_html(b"[example](https://example.com)").unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("href=\"https://example.com\""), "got: {s}");
assert!(s.contains("example"), "got: {s}");
}
#[test]
fn render_image() {
let html = root::render_to_html(b"").unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<img"), "got: {s}");
assert!(s.contains("src=\"img.png\""), "got: {s}");
}
#[test]
fn render_unordered_list() {
let input = b"- one\n- two\n- three";
let html = root::render_to_html(input).unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<ul>"), "got: {s}");
assert!(s.contains("<li>one</li>"), "got: {s}");
}
#[test]
fn render_ordered_list() {
let input = b"1. first\n2. second\n3. third";
let html = root::render_to_html(input).unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<ol>"), "got: {s}");
assert!(s.contains("<li>first</li>"), "got: {s}");
}
#[test]
fn render_blockquote() {
let html = root::render_to_html(b"> quoted text").unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<blockquote>"), "got: {s}");
assert!(s.contains("quoted text"), "got: {s}");
}
#[test]
fn render_horizontal_rule() {
let html = root::render_to_html(b"---").unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<hr"), "got: {s}");
}
#[test]
fn render_table() {
let input = b"| A | B |\n| --- | --- |\n| 1 | 2 |";
let html = root::render_to_html_with_options(input, Options::GITHUB).unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<table>"), "got: {s}");
assert!(s.contains("<th>"), "got: {s}");
assert!(s.contains("<td>"), "got: {s}");
}
#[test]
fn render_strikethrough() {
let html = root::render_to_html_with_options(b"~~deleted~~", Options::GITHUB).unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<del>deleted</del>"), "got: {s}");
}
#[test]
fn render_tasklist() {
let input = b"- [x] done\n- [ ] todo";
let html = root::render_to_html_with_options(input, Options::GITHUB).unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("x") || s.contains("checked"), "got: {s}");
}
#[test]
fn options_commonmark_no_extensions() {
let opts = Options::COMMONMARK;
assert!(!opts.tables);
assert!(!opts.strikethrough);
assert!(!opts.tasklists);
}
#[test]
fn options_github_enables_gfm() {
let opts = Options::GITHUB;
assert!(opts.tables);
assert!(opts.strikethrough);
assert!(opts.tasklists);
assert!(opts.tag_filter);
}
#[test]
fn options_terminal_enables_math() {
let opts = Options::TERMINAL;
assert!(opts.latex_math);
assert!(opts.wiki_links);
}
#[test]
fn render_empty_input() {
let html = root::render_to_html(b"").unwrap();
assert!(html.is_empty() || html.len() < 10, "got: {:?}", std::str::from_utf8(&html));
}
#[test]
fn render_utf8_bom_stripped() {
let input: &[u8] = &[0xEF, 0xBB, 0xBF, b'H', b'i'];
let html = root::render_to_html(input).unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("Hi"), "got: {s}");
assert!(!s.contains("\u{feff}"), "BOM should be stripped, got: {s}");
}
#[test]
fn render_hard_break() {
let html = root::render_to_html(b"line1 \nline2").unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("<br"), "got: {s}");
}
#[test]
fn render_soft_break() {
let html = root::render_to_html(b"line1\nline2").unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("line1") && s.contains("line2"), "got: {s}");
}
#[test]
fn render_heading_ids() {
let opts = Options {
heading_ids: true,
..Options::GITHUB
};
let html = root::render_to_html_with_options(b"# My Title", opts).unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("id=") || s.contains("my-title"), "got: {s}");
}
#[test]
fn render_heading_ids_deduplication() {
let opts = Options {
heading_ids: true,
..Options::GITHUB
};
let input = b"# Title\n## Title";
let html = root::render_to_html_with_options(input, opts).unwrap();
let s = std::str::from_utf8(&html).unwrap();
assert!(s.contains("title-1") || s.matches("title").count() >= 2, "got: {s}");
}
use bun_md::types::{BlockType, Renderer, RendererImpl, SpanDetail, SpanType, TextType, BLOCK_FENCED_CODE};
struct RecordingRenderer {
events: Vec<String>,
}
impl RendererImpl for RecordingRenderer {
fn enter_block(&mut self, bt: BlockType, data: u32, flags: u32) -> bun_md::types::JsResult<()> {
self.events.push(format!("enter_block({:?} data={} flags={})", bt, data, flags));
Ok(())
}
fn leave_block(&mut self, bt: BlockType, _data: u32) -> bun_md::types::JsResult<()> {
self.events.push(format!("leave_block({:?})", bt));
Ok(())
}
fn enter_span(&mut self, st: SpanType, detail: SpanDetail<'_>) -> bun_md::types::JsResult<()> {
self.events.push(format!("enter_span({:?} href={:?})", st, std::str::from_utf8(detail.href).unwrap_or("")));
Ok(())
}
fn leave_span(&mut self, st: SpanType) -> bun_md::types::JsResult<()> {
self.events.push(format!("leave_span({:?})", st));
Ok(())
}
fn text(&mut self, tt: TextType, content: &[u8]) -> bun_md::types::JsResult<()> {
self.events.push(format!("text({:?} {:?})", tt, std::str::from_utf8(content).unwrap_or("")));
Ok(())
}
}
#[test]
fn bridge_paragraph_events() {
let mut recorder = RecordingRenderer { events: Vec::new() };
let renderer = Renderer { ptr: &mut recorder };
root::render_with_renderer(b"Hello world", Options::default(), renderer).unwrap();
assert!(recorder.events.iter().any(|e| e.contains("enter_block(P")));
assert!(recorder.events.iter().any(|e| e.contains("Hello world")));
assert!(recorder.events.iter().any(|e| e.contains("leave_block(P")));
}
#[test]
fn bridge_heading_level() {
let mut recorder = RecordingRenderer { events: Vec::new() };
let renderer = Renderer { ptr: &mut recorder };
root::render_with_renderer(b"## Title", Options::default(), renderer).unwrap();
let heading_enter = recorder.events.iter().find(|e| e.contains("enter_block(H"));
assert!(heading_enter.is_some(), "expected enter_block(H) event, got: {:?}", recorder.events);
assert!(heading_enter.unwrap().contains("data=2"), "expected heading level 2");
}
#[test]
fn bridge_link_href() {
let mut recorder = RecordingRenderer { events: Vec::new() };
let renderer = Renderer { ptr: &mut recorder };
root::render_with_renderer(b"[click](https://example.com)", Options::default(), renderer).unwrap();
let link_enter = recorder.events.iter().find(|e| e.contains("enter_span(A"));
assert!(link_enter.is_some(), "expected enter_span(A), got: {:?}", recorder.events);
assert!(link_enter.unwrap().contains("https://example.com"), "expected href in link detail");
}
#[test]
fn bridge_fenced_code_flags() {
let mut recorder = RecordingRenderer { events: Vec::new() };
let renderer = Renderer { ptr: &mut recorder };
root::render_with_renderer(b"```javascript\ncode\n```", Options::default(), renderer).unwrap();
let code_enter = recorder.events.iter().find(|e| e.contains("enter_block(Code"));
assert!(code_enter.is_some(), "expected enter_block(Code)");
assert!(code_enter.unwrap().contains(&format!("flags={}", BLOCK_FENCED_CODE)), "expected fenced code flag");
}
#[test]
fn bridge_table_events() {
let mut recorder = RecordingRenderer { events: Vec::new() };
let renderer = Renderer { ptr: &mut recorder };
root::render_with_renderer(b"| A | B |\n| - | - |\n| 1 | 2 |", Options::default(), renderer).unwrap();
assert!(recorder.events.iter().any(|e| e.contains("enter_block(Table")));
assert!(recorder.events.iter().any(|e| e.contains("enter_block(Thead")));
assert!(recorder.events.iter().any(|e| e.contains("enter_block(Th")));
assert!(recorder.events.iter().any(|e| e.contains("enter_block(Td")));
}
#[test]
fn bridge_hr_events() {
let mut recorder = RecordingRenderer { events: Vec::new() };
let renderer = Renderer { ptr: &mut recorder };
root::render_with_renderer(b"---", Options::default(), renderer).unwrap();
assert!(recorder.events.iter().any(|e| e.contains("enter_block(Hr")));
assert!(recorder.events.iter().any(|e| e.contains("leave_block(Hr")));
}
#[test]
fn bridge_ordered_list_start() {
let mut recorder = RecordingRenderer { events: Vec::new() };
let renderer = Renderer { ptr: &mut recorder };
root::render_with_renderer(b"3. first\n4. second", Options::default(), renderer).unwrap();
let ol_enter = recorder.events.iter().find(|e| e.contains("enter_block(Ol"));
assert!(ol_enter.is_some(), "expected enter_block(Ol)");
assert!(ol_enter.unwrap().contains("data=3"), "expected start=3");
}
#[test]
fn bridge_strikethrough_span() {
let mut recorder = RecordingRenderer { events: Vec::new() };
let renderer = Renderer { ptr: &mut recorder };
root::render_with_renderer(b"~~deleted~~", Options::GITHUB, renderer).unwrap();
assert!(recorder.events.iter().any(|e| e.contains("enter_span(Del")));
assert!(recorder.events.iter().any(|e| e.contains("leave_span(Del")));
}
#[test]
fn bridge_image_span() {
let mut recorder = RecordingRenderer { events: Vec::new() };
let renderer = Renderer { ptr: &mut recorder };
root::render_with_renderer(b"", Options::default(), renderer).unwrap();
let img_enter = recorder.events.iter().find(|e| e.contains("enter_span(Img"));
assert!(img_enter.is_some(), "expected enter_span(Img), got: {:?}", recorder.events);
assert!(img_enter.unwrap().contains("photo.png"), "expected src in img detail");
}