#![allow(clippy::expect_used, clippy::indexing_slicing, clippy::unwrap_used)]
mod helpers {
#![allow(clippy::single_call_fn)]
use docspec_core::{Event, ImageSource, ListStyleType, TableHeaderScope, TextStyle};
pub fn start_document() -> Event {
Event::StartDocument {
id: None,
language: None,
metadata: None,
}
}
pub fn start_paragraph() -> Event {
Event::StartParagraph {
alignment: None,
id: None,
}
}
pub fn start_heading(level: u8) -> Event {
Event::StartHeading { level, id: None }
}
pub fn text(content: &str, style: TextStyle) -> Event {
Event::Text {
content: content.to_string(),
style,
}
}
pub fn start_preformatted(syntax: Option<&str>) -> Event {
Event::StartPreformatted {
syntax: syntax.map(str::to_string),
id: None,
}
}
pub fn start_table() -> Event {
Event::StartTable { id: None }
}
pub fn start_table_row() -> Event {
Event::StartTableRow { id: None }
}
pub fn start_table_header() -> Event {
Event::StartTableHeader {
abbr: None,
colspan: None,
rowspan: None,
scope: Some(TableHeaderScope::Column),
id: None,
}
}
pub fn start_table_cell() -> Event {
Event::StartTableCell {
colspan: None,
rowspan: None,
id: None,
}
}
pub fn start_unordered_list_item(level: u32) -> Event {
Event::StartUnorderedListItem {
style_type: ListStyleType::Disc,
level,
id: None,
}
}
pub fn start_ordered_list_item(level: u32, start: Option<u64>) -> Event {
Event::StartOrderedListItem {
start,
style_type: ListStyleType::Decimal,
level,
id: None,
}
}
pub fn image_uri(uri: &str, alt: Option<&str>, title: Option<&str>, decorative: bool) -> Event {
Event::Image {
source: ImageSource::Uri {
uri: uri.to_string(),
},
alt: alt.map(str::to_string),
title: title.map(str::to_string),
decorative,
id: None,
}
}
}
#[cfg(test)]
mod tests {
use super::helpers;
use docspec_core::{Event, EventSource as _, ImageSource, ListStyleType, TextStyle};
use docspec_markdown_reader::MarkdownReader;
fn collect_events(reader: &mut MarkdownReader<'_>) -> Vec<Event> {
let mut events = Vec::new();
loop {
let result = reader.next_event();
assert!(result.is_ok(), "next_event failed: {:?}", result.err());
match result {
Ok(Some(event)) => events.push(event),
Ok(None) | Err(_) => break,
}
}
events
}
#[test]
fn blockquote_content_extraction() {
let markdown = "> Quoted text";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartBlockQuote { id: None },
Event::StartParagraph {
alignment: None,
id: None,
},
Event::Text {
content: "Quoted text".to_string(),
style: TextStyle::default(),
},
Event::EndParagraph,
Event::EndBlockQuote,
Event::EndDocument,
]
);
}
#[test]
fn bold_and_italic_text() {
let mut reader = MarkdownReader::new("***both***");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("both", TextStyle::default().bold().italic()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn bold_text() {
let mut reader = MarkdownReader::new("**bold**");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("bold", TextStyle::default().bold()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn document_structure_preserved() {
let markdown = "# Title\n\nParagraph text.\n\n---\n\n## Subtitle";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_heading(1),
helpers::text("Title", TextStyle::default()),
Event::EndHeading,
helpers::start_paragraph(),
helpers::text("Paragraph text.", TextStyle::default()),
Event::EndParagraph,
Event::ThematicBreak { id: None },
helpers::start_heading(2),
helpers::text("Subtitle", TextStyle::default()),
Event::EndHeading,
Event::EndDocument,
]
);
}
#[test]
fn empty_document() {
let mut reader = MarkdownReader::new("");
let events = collect_events(&mut reader);
assert_eq!(events, vec![helpers::start_document(), Event::EndDocument]);
}
#[test]
fn hard_break() {
let mut reader = MarkdownReader::new("Line one \nLine two");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("Line one", TextStyle::default()),
Event::LineBreak,
helpers::text("Line two", TextStyle::default()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn heading_level_1() {
let mut reader = MarkdownReader::new("# Hello");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_heading(1),
helpers::text("Hello", TextStyle::default()),
Event::EndHeading,
Event::EndDocument,
]
);
}
#[test]
fn heading_levels_2_through_6() {
let expected_levels: [u8; 5] = [2, 3, 4, 5, 6];
for expected in expected_levels {
let markdown = format!("{} Heading", "#".repeat(usize::from(expected)));
let mut reader = MarkdownReader::new(&markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_heading(expected),
helpers::text("Heading", TextStyle::default()),
Event::EndHeading,
Event::EndDocument,
]
);
}
}
#[test]
fn image_with_alt_and_title() {
let mut reader =
MarkdownReader::new("");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::image_uri(
"https://example.com/img.png",
Some("Alt text"),
Some("Image Title"),
false,
),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn image_with_alt_only() {
let mut reader = MarkdownReader::new("");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::image_uri(
"https://example.com/img.png",
Some("Alt text only"),
None,
false,
),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn image_with_no_alt() {
let mut reader = MarkdownReader::new("");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::image_uri("https://example.com/img.png", None, None, true),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn images_fixture() {
let markdown = include_str!("../../../tests/fixtures/markdown/images.md");
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::image_uri(
"https://example.com/image1.png",
Some("Alt text with title"),
Some("Image Title"),
false,
),
Event::EndParagraph,
helpers::start_paragraph(),
helpers::image_uri(
"https://example.com/image2.png",
Some("Alt text only"),
None,
false,
),
Event::EndParagraph,
helpers::start_paragraph(),
helpers::image_uri("https://example.com/image3.png", None, None, true),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn inline_code() {
let mut reader = MarkdownReader::new("Use `code` here");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("Use ", TextStyle::default()),
helpers::text("code", TextStyle::default().code()),
helpers::text(" here", TextStyle::default()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn inline_code_inherits_bold() {
let mut reader = MarkdownReader::new("**bold `code` bold**");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("bold ", TextStyle::default().bold()),
helpers::text("code", TextStyle::default().bold().code()),
helpers::text(" bold", TextStyle::default().bold()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn inline_code_inherits_italic() {
let mut reader = MarkdownReader::new("*italic `code` italic*");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("italic ", TextStyle::default().italic()),
helpers::text("code", TextStyle::default().italic().code()),
helpers::text(" italic", TextStyle::default().italic()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn inline_code_inherits_strikethrough() {
let mut reader = MarkdownReader::new("~~strikethrough `code` strikethrough~~");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("strikethrough ", TextStyle::default().strikethrough()),
helpers::text("code", TextStyle::default().strikethrough().code()),
helpers::text(" strikethrough", TextStyle::default().strikethrough()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn italic_text() {
let mut reader = MarkdownReader::new("*italic*");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("italic", TextStyle::default().italic()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn list_content_extraction() {
let markdown = "- Item one\n- Item two";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_unordered_list_item(0),
Event::Text {
content: "Item one".to_string(),
style: TextStyle::default(),
},
Event::EndUnorderedListItem,
helpers::start_unordered_list_item(0),
Event::Text {
content: "Item two".to_string(),
style: TextStyle::default(),
},
Event::EndUnorderedListItem,
Event::EndDocument,
]
);
}
#[test]
fn nested_content_fixture() {
let markdown = include_str!("../../../tests/fixtures/markdown/nested_content.md");
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
let text_contents: Vec<&str> = events
.iter()
.filter_map(|event| {
if let Event::Text { content, .. } = event {
Some(content.as_str())
} else {
None
}
})
.collect();
assert_eq!(
text_contents,
vec!["Header", "This paragraph is inside a table cell."]
);
}
#[test]
fn nested_list_with_continuation_keeps_parent_item_open() {
let mut reader = MarkdownReader::new(
"- Outer A\n - Inner nested\n\n Continuation paragraph for Outer A\n- Outer B\n",
);
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(events.get(2), Some(&helpers::start_paragraph()));
assert_eq!(events.get(4), Some(&Event::EndParagraph));
assert_eq!(events.get(5), Some(&helpers::start_unordered_list_item(1)));
assert_eq!(events.get(7), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(8), Some(&helpers::start_paragraph()));
assert_eq!(events.get(10), Some(&Event::EndParagraph));
assert_eq!(events.get(11), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(12), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(events.len(), 18);
}
#[test]
fn next_event_returns_none_after_end_document() {
let mut reader = MarkdownReader::new("");
let events = collect_events(&mut reader);
assert_eq!(events.len(), 2);
let first_after_end = reader.next_event();
assert!(
first_after_end.is_ok(),
"next_event failed: {first_after_end:?}"
);
assert_eq!(first_after_end.unwrap(), None);
let second_after_end = reader.next_event();
assert!(
second_after_end.is_ok(),
"next_event failed: {second_after_end:?}"
);
assert_eq!(second_after_end.unwrap(), None);
}
#[test]
fn paragraph() {
let mut reader = MarkdownReader::new("Hello world");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("Hello world", TextStyle::default()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn soft_break_emits_soft_break_event() {
let mut reader = MarkdownReader::new("Line one\nLine two");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("Line one", TextStyle::default()),
Event::SoftBreak,
helpers::text("Line two", TextStyle::default()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn soft_break_in_image_alt_appends_space() {
let mut reader = MarkdownReader::new("");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::image_uri("image.png", Some("alt one alt two"), None, false),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn thematic_break() {
let mut reader = MarkdownReader::new("Before\n\n---\n\nAfter");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("Before", TextStyle::default()),
Event::EndParagraph,
Event::ThematicBreak { id: None },
helpers::start_paragraph(),
helpers::text("After", TextStyle::default()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn code_in_image_alt_appends_to_alt_buffer() {
let mut reader = MarkdownReader::new("");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::image_uri("https://example.com/img.png", Some("code"), None, false),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn html_events_silently_ignored() {
let mut reader = MarkdownReader::new("<div>hello</div>");
let events = collect_events(&mut reader);
assert_eq!(events, vec![helpers::start_document(), Event::EndDocument]);
}
#[test]
fn blockquote_text_wrapped_in_auto_paragraph() {
let markdown = "> Quoted";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartBlockQuote { id: None },
helpers::start_paragraph(),
helpers::text("Quoted", TextStyle::default()),
Event::EndParagraph,
Event::EndBlockQuote,
Event::EndDocument,
]
);
}
#[test]
fn list_item_emits_start_text_end_directly() {
let markdown = "- Item";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_unordered_list_item(0),
helpers::text("Item", TextStyle::default()),
Event::EndUnorderedListItem,
Event::EndDocument,
]
);
}
#[test]
fn fenced_code_block_with_language() {
let markdown = "```rust\nfn main() {}\n```";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_preformatted(Some("rust")),
helpers::text("fn main() {}", TextStyle::default()),
Event::EndPreformatted,
Event::EndDocument,
]
);
}
#[test]
fn fenced_code_block_without_language() {
let markdown = "```\nsome code\n```";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_preformatted(None),
helpers::text("some code", TextStyle::default()),
Event::EndPreformatted,
Event::EndDocument,
]
);
}
#[test]
fn indented_code_block() {
let markdown = " indented code\n";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_preformatted(None),
helpers::text("indented code", TextStyle::default()),
Event::EndPreformatted,
Event::EndDocument,
]
);
}
#[test]
fn code_block_preserves_trailing_blank_lines() {
let markdown = "```\ncode\n\n\n```";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_preformatted(None),
helpers::text("code\n\n", TextStyle::default()),
Event::EndPreformatted,
Event::EndDocument,
]
);
}
#[test]
fn code_block_special_chars_pass_through_literally() {
let markdown = "```\n*test* **bold** `code` _italic_ ~strike~\n```";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_preformatted(None),
helpers::text(
"*test* **bold** `code` _italic_ ~strike~",
TextStyle::default(),
),
Event::EndPreformatted,
Event::EndDocument,
]
);
}
#[test]
fn fenced_code_block_empty() {
let markdown = "```\n```";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_preformatted(None),
Event::EndPreformatted,
Event::EndDocument,
]
);
}
#[test]
fn code_block_inside_list_item() {
let markdown = "- item\n\n ```\n code\n ```";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_unordered_list_item(0),
helpers::start_paragraph(),
helpers::text("item", TextStyle::default()),
Event::EndParagraph,
helpers::start_preformatted(None),
helpers::text("code", TextStyle::default()),
Event::EndPreformatted,
Event::EndUnorderedListItem,
Event::EndDocument,
]
);
}
#[test]
fn strikethrough_basic() {
let mut reader = MarkdownReader::new("~~struck~~");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("struck", TextStyle::default().strikethrough()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn strikethrough_with_bold() {
let mut reader = MarkdownReader::new("~~**bold struck**~~");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("bold struck", TextStyle::default().bold().strikethrough()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn strikethrough_with_italic() {
let mut reader = MarkdownReader::new("~~*italic struck*~~");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text(
"italic struck",
TextStyle::default().italic().strikethrough()
),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn strikethrough_in_paragraph() {
let markdown = "This is ~~struck~~ text in a paragraph.";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("This is ", TextStyle::default()),
helpers::text("struck", TextStyle::default().strikethrough()),
helpers::text(" text in a paragraph.", TextStyle::default()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn strikethrough_with_bold_and_italic() {
let mut reader = MarkdownReader::new("~~***bold italic struck***~~");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text(
"bold italic struck",
TextStyle::default().bold().italic().strikethrough(),
),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn simple_table_emits_structured_events() {
let markdown = "| A | B |\n|---|---|\n| C | D |";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_table()));
assert_eq!(events.get(2), Some(&helpers::start_table_row()));
assert_eq!(events.get(3), Some(&helpers::start_table_header()));
assert_eq!(events.get(9), Some(&Event::EndTableRow));
assert_eq!(events.get(10), Some(&helpers::start_table_row()));
assert_eq!(events.get(18), Some(&Event::EndTable));
}
#[test]
fn table_header_cells_have_column_scope() {
let markdown = "| H1 | H2 |\n|----|----|";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(events.get(3), Some(&helpers::start_table_header()));
assert_eq!(events.get(6), Some(&helpers::start_table_header()));
}
#[test]
fn table_body_cells_have_no_scope_field() {
let markdown = "| H |\n|---|\n| C |";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(events.get(3), Some(&helpers::start_table_header()));
assert_eq!(events.get(7), Some(&helpers::start_table_row()));
assert_eq!(events.get(8), Some(&helpers::start_table_cell()));
}
#[test]
fn table_cell_text_emits_raw_not_wrapped() {
let markdown = "| H |\n|---|\n| text |";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(events.get(8), Some(&helpers::start_table_cell()));
assert_eq!(
events.get(9),
Some(&helpers::text("text", TextStyle::default()))
);
assert_eq!(events.get(10), Some(&Event::EndTableCell));
}
#[test]
fn table_with_inline_formatting_in_cells() {
let markdown = "| **bold** | `code` |\n|-----------|--------|\n| *italic* | plain |";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events.get(4),
Some(&helpers::text("bold", TextStyle::default().bold()))
);
assert_eq!(
events.get(7),
Some(&helpers::text("code", TextStyle::default().code()))
);
assert_eq!(
events.get(12),
Some(&helpers::text("italic", TextStyle::default().italic()))
);
}
#[test]
fn header_only_table() {
let markdown = "| H1 | H2 |\n|----|----|";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_table()));
assert_eq!(events.get(2), Some(&helpers::start_table_row()));
assert_eq!(events.get(9), Some(&Event::EndTableRow));
assert_eq!(events.get(10), Some(&Event::EndTable));
assert_eq!(events.get(11), Some(&Event::EndDocument));
}
#[test]
fn table_with_empty_cells() {
let markdown = "| A | |\n|---|---|\n| | B |";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(events.get(6), Some(&helpers::start_table_header()));
assert_eq!(events.get(7), Some(&Event::EndTableHeader));
assert_eq!(events.get(10), Some(&helpers::start_table_cell()));
assert_eq!(events.get(11), Some(&Event::EndTableCell));
}
#[test]
fn multiple_tables_in_sequence() {
let markdown = "| A |\n|---|\n| B |\n\n| C |\n|---|\n| D |";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_table()));
assert_eq!(events.get(12), Some(&Event::EndTable));
assert_eq!(events.get(13), Some(&helpers::start_table()));
assert_eq!(events.get(24), Some(&Event::EndTable));
}
#[test]
fn simple_bullet_list_emits_unordered_items() {
let mut reader = MarkdownReader::new("- a\n- b\n- c");
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(
events.get(2),
Some(&helpers::text("a", TextStyle::default()))
);
assert_eq!(events.get(3), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(4), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(events.get(6), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(7), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(events.get(9), Some(&Event::EndUnorderedListItem));
}
#[test]
fn simple_numbered_list_emits_ordered_items() {
let mut reader = MarkdownReader::new("1. a\n2. b\n3. c");
let events = collect_events(&mut reader);
assert_eq!(
events.get(1),
Some(&helpers::start_ordered_list_item(0, Some(1)))
);
assert_eq!(events.get(3), Some(&Event::EndOrderedListItem));
assert_eq!(
events.get(4),
Some(&helpers::start_ordered_list_item(0, None))
);
assert_eq!(
events.get(7),
Some(&helpers::start_ordered_list_item(0, None))
);
}
#[test]
fn numbered_list_with_explicit_start() {
let mut reader = MarkdownReader::new("5. a\n6. b\n7. c");
let events = collect_events(&mut reader);
assert_eq!(
events.get(1),
Some(&helpers::start_ordered_list_item(0, Some(5)))
);
assert_eq!(
events.get(4),
Some(&helpers::start_ordered_list_item(0, None))
);
assert_eq!(
events.get(7),
Some(&helpers::start_ordered_list_item(0, None))
);
}
#[test]
fn bullet_list_emits_disc_style() {
let mut reader = MarkdownReader::new("- a");
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_unordered_list_item(0)));
}
#[test]
fn ordered_list_emits_decimal_style() {
let mut reader = MarkdownReader::new("1. a");
let events = collect_events(&mut reader);
assert_eq!(
events.get(1),
Some(&helpers::start_ordered_list_item(0, Some(1)))
);
}
#[test]
fn nested_bullet_lists_emit_increasing_level() {
let mut reader = MarkdownReader::new("- A\n - B\n - C\n- D");
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(events.get(3), Some(&helpers::start_unordered_list_item(1)));
assert_eq!(events.get(5), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(6), Some(&helpers::start_unordered_list_item(1)));
assert_eq!(events.get(8), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(9), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(10), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(events.get(12), Some(&Event::EndUnorderedListItem));
}
#[test]
fn mixed_nested_lists() {
let mut reader = MarkdownReader::new("- A\n 1. B\n 2. C\n- D");
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(
events.get(3),
Some(&helpers::start_ordered_list_item(1, Some(1)))
);
assert_eq!(events.get(5), Some(&Event::EndOrderedListItem));
assert_eq!(
events.get(6),
Some(&helpers::start_ordered_list_item(1, None))
);
assert_eq!(events.get(8), Some(&Event::EndOrderedListItem));
assert_eq!(events.get(9), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(10), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(events.get(12), Some(&Event::EndUnorderedListItem));
}
#[test]
fn list_item_containing_only_nested_list_no_text() {
let mut reader = MarkdownReader::new("- \n - Nested item\n- Sibling\n");
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(events.get(2), Some(&helpers::start_unordered_list_item(1)));
assert_eq!(
events.get(3),
Some(&helpers::text("Nested item", TextStyle::default()))
);
assert_eq!(events.get(4), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(5), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(6), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(events.get(8), Some(&Event::EndUnorderedListItem));
assert_eq!(events.len(), 10);
}
#[test]
fn list_items_with_inline_formatting() {
let mut reader = MarkdownReader::new("- **bold** item\n- *italic* item");
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(
events.get(2),
Some(&helpers::text("bold", TextStyle::default().bold()))
);
assert_eq!(events.get(4), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(5), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(
events.get(6),
Some(&helpers::text("italic", TextStyle::default().italic()))
);
assert_eq!(events.get(8), Some(&Event::EndUnorderedListItem));
}
#[test]
fn nested_list_with_hard_break_in_continuation() {
let mut reader = MarkdownReader::new("- A\n - B\n\n Line one \n Line two\n");
let events = collect_events(&mut reader);
assert_eq!(events.get(8), Some(&helpers::start_paragraph()));
assert_eq!(
events.get(9),
Some(&helpers::text("Line one", TextStyle::default()))
);
assert_eq!(events.get(10), Some(&Event::LineBreak));
assert_eq!(
events.get(11),
Some(&helpers::text("Line two", TextStyle::default()))
);
assert_eq!(events.get(12), Some(&Event::EndParagraph));
assert_eq!(events.get(13), Some(&Event::EndUnorderedListItem));
assert_eq!(events.len(), 15);
}
#[test]
fn three_levels_deeply_nested_unordered_list() {
let mut reader = MarkdownReader::new("- A\n - B\n - C\n");
let events = collect_events(&mut reader);
assert_eq!(events.get(1), Some(&helpers::start_unordered_list_item(0)));
assert_eq!(events.get(3), Some(&helpers::start_unordered_list_item(1)));
assert_eq!(events.get(5), Some(&helpers::start_unordered_list_item(2)));
assert_eq!(
events.get(6),
Some(&helpers::text("C", TextStyle::default()))
);
assert_eq!(events.get(7), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(8), Some(&Event::EndUnorderedListItem));
assert_eq!(events.get(9), Some(&Event::EndUnorderedListItem));
assert_eq!(events.len(), 11);
}
#[test]
fn list_inside_blockquote_inside_list_item() {
let markdown = "5) I2\n > text\n > - [f]\n";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartOrderedListItem {
start: Some(5),
style_type: ListStyleType::Decimal,
level: 0,
id: None,
},
Event::Text {
content: "I2".to_string(),
style: TextStyle::default(),
},
Event::StartBlockQuote { id: None },
Event::StartParagraph {
alignment: None,
id: None,
},
Event::Text {
content: "text".to_string(),
style: TextStyle::default(),
},
Event::EndParagraph,
helpers::start_unordered_list_item(1),
Event::Text {
content: "[".to_string(),
style: TextStyle::default(),
},
Event::Text {
content: "f".to_string(),
style: TextStyle::default(),
},
Event::Text {
content: "]".to_string(),
style: TextStyle::default(),
},
Event::EndUnorderedListItem,
Event::EndBlockQuote,
Event::EndOrderedListItem,
Event::EndDocument,
]
);
}
#[test]
fn link_simple() {
let markdown = "[text](https://example.com)";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartParagraph {
alignment: None,
id: None,
},
Event::StartLink {
href: "https://example.com".to_string(),
title: None,
id: None,
},
Event::Text {
content: "text".to_string(),
style: TextStyle::default(),
},
Event::EndLink,
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn link_with_title() {
let markdown = r#"[text](https://example.com "a title")"#;
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartParagraph {
alignment: None,
id: None,
},
Event::StartLink {
href: "https://example.com".to_string(),
title: Some("a title".to_string()),
id: None,
},
Event::Text {
content: "text".to_string(),
style: TextStyle::default(),
},
Event::EndLink,
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn link_empty_text() {
let markdown = "[](https://example.com)";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartParagraph {
alignment: None,
id: None,
},
Event::StartLink {
href: "https://example.com".to_string(),
title: None,
id: None,
},
Event::EndLink,
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn link_styled_content() {
let markdown = "[**bold** text](https://example.com)";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartParagraph {
alignment: None,
id: None,
},
Event::StartLink {
href: "https://example.com".to_string(),
title: None,
id: None,
},
Event::Text {
content: "bold".to_string(),
style: TextStyle::default().bold(),
},
Event::Text {
content: " text".to_string(),
style: TextStyle::default(),
},
Event::EndLink,
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn link_with_code_span() {
let markdown = "[`code`](https://example.com)";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartParagraph {
alignment: None,
id: None,
},
Event::StartLink {
href: "https://example.com".to_string(),
title: None,
id: None,
},
Event::Text {
content: "code".to_string(),
style: TextStyle::default().code(),
},
Event::EndLink,
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn autolink() {
let markdown = "<https://example.com>";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartParagraph {
alignment: None,
id: None,
},
Event::StartLink {
href: "https://example.com".to_string(),
title: None,
id: None,
},
Event::Text {
content: "https://example.com".to_string(),
style: TextStyle::default(),
},
Event::EndLink,
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn link_in_heading() {
let markdown = "# [text](https://example.com)";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartHeading { level: 1, id: None },
Event::StartLink {
href: "https://example.com".to_string(),
title: None,
id: None,
},
Event::Text {
content: "text".to_string(),
style: TextStyle::default(),
},
Event::EndLink,
Event::EndHeading,
Event::EndDocument,
]
);
}
#[test]
fn link_in_paragraph_with_surrounding_text() {
let markdown = "before [link text](https://example.com) after";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartParagraph {
alignment: None,
id: None,
},
Event::Text {
content: "before ".to_string(),
style: TextStyle::default(),
},
Event::StartLink {
href: "https://example.com".to_string(),
title: None,
id: None,
},
Event::Text {
content: "link text".to_string(),
style: TextStyle::default(),
},
Event::EndLink,
Event::Text {
content: " after".to_string(),
style: TextStyle::default(),
},
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn image_in_link_extracts_image_as_sibling() {
let markdown = "[](https://example.com)";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
Event::StartParagraph {
alignment: None,
id: None,
},
Event::StartLink {
href: "https://example.com".to_string(),
title: None,
id: None,
},
Event::EndLink,
Event::Image {
source: ImageSource::Uri {
uri: "img.png".to_string(),
},
alt: Some("alt".to_string()),
title: None,
decorative: false,
id: None,
},
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn inline_html_only_paragraph_emits_nothing() {
let mut reader = MarkdownReader::new("<span id=\"ferris\"></span>");
let events = collect_events(&mut reader);
assert_eq!(events, vec![helpers::start_document(), Event::EndDocument]);
}
#[test]
fn inline_html_between_paragraphs_preserves_surrounding() {
let markdown = "Before\n\n<span id=\"ferris\"></span>\n\nAfter";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("Before", TextStyle::default()),
Event::EndParagraph,
helpers::start_paragraph(),
helpers::text("After", TextStyle::default()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn inline_html_with_text_still_emits_paragraph() {
let markdown = "text <span></span> more";
let mut reader = MarkdownReader::new(markdown);
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::text("text ", TextStyle::default()),
helpers::text(" more", TextStyle::default()),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn inline_html_inside_emphasis_emits_nothing() {
let mut reader = MarkdownReader::new("*<span></span>*");
let events = collect_events(&mut reader);
assert_eq!(events, vec![helpers::start_document(), Event::EndDocument]);
}
#[test]
fn softbreak_only_after_html_filter_emits_nothing() {
let mut reader = MarkdownReader::new("<span></span>\n<span></span>");
let events = collect_events(&mut reader);
assert_eq!(events, vec![helpers::start_document(), Event::EndDocument]);
}
#[test]
fn image_only_paragraph_emits_paragraph_wrapper() {
let mut reader = MarkdownReader::new("");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_paragraph(),
helpers::image_uri("img.png", Some("alt"), None, false),
Event::EndParagraph,
Event::EndDocument,
]
);
}
#[test]
fn hardbreak_only_paragraph_emits_nothing() {
let mut reader = MarkdownReader::new(" \n");
let events = collect_events(&mut reader);
assert_eq!(events, vec![helpers::start_document(), Event::EndDocument]);
}
#[test]
fn inline_html_with_hardbreak_emits_nothing() {
let mut reader = MarkdownReader::new("<span></span> \n<span></span>");
let events = collect_events(&mut reader);
assert_eq!(events, vec![helpers::start_document(), Event::EndDocument]);
}
#[test]
fn list_item_with_only_softbreak_emits_softbreak() {
let mut reader = MarkdownReader::new("- <span></span>\n <span></span>");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_unordered_list_item(0),
Event::SoftBreak,
Event::EndUnorderedListItem,
Event::EndDocument,
]
);
}
#[test]
fn list_item_with_text_and_softbreak_emits_text_and_break() {
let mut reader = MarkdownReader::new("- text\n more");
let events = collect_events(&mut reader);
assert_eq!(
events,
vec![
helpers::start_document(),
helpers::start_unordered_list_item(0),
helpers::text("text", TextStyle::default()),
Event::SoftBreak,
helpers::text("more", TextStyle::default()),
Event::EndUnorderedListItem,
Event::EndDocument,
]
);
}
}