use std::borrow::Cow;
use crate::section::OrderedListDelimiter;
use crate::{Inline, InlineSpan, MarkdownFile, Section};
struct Checker<'md, 'src> {
md: &'md MarkdownFile<'src>,
}
impl<'md, 'src> Checker<'md, 'src> {
fn new(md: &'md MarkdownFile<'src>) -> Self {
Self { md }
}
fn check_span(&self, span: InlineSpan, expected: &[Expect]) {
let actual = self.md.inlines(span);
assert_eq!(
actual.len(),
expected.len(),
"span length mismatch: got {actual:?}, expected {expected:?}"
);
for (i, (a, e)) in actual.iter().zip(expected).enumerate() {
self.check_inline(a, e, i);
}
}
fn check_inline(&self, actual: &Inline, expected: &Expect, idx: usize) {
match (actual, expected) {
(Inline::Text(a), Expect::Text(e)) => {
assert_eq!(a, e, "Text mismatch at index {idx}");
}
(Inline::Bold(span), Expect::Bold(children))
| (Inline::Italic(span), Expect::Italic(children)) => {
self.check_span(*span, children);
}
(Inline::Code(a), Expect::Code(e)) => {
assert_eq!(a, e, "Code mismatch at index {idx}");
}
(Inline::SoftBreak, Expect::SoftBreak) | (Inline::HardBreak, Expect::HardBreak) => {}
(
Inline::Link { text, url, title },
Expect::Link {
text: et,
url: eu,
title: eti,
},
) => {
assert_eq!(url, eu, "Link url mismatch at index {idx}");
assert_eq!(title, eti, "Link title mismatch at index {idx}");
self.check_span(*text, et);
}
(
Inline::Image { alt, url, title },
Expect::Image {
alt: ea,
url: eu,
title: eti,
},
) => {
assert_eq!(alt, ea, "Image alt mismatch at index {idx}");
assert_eq!(url, eu, "Image url mismatch at index {idx}");
assert_eq!(title, eti, "Image title mismatch at index {idx}");
}
_ => panic!("Inline mismatch at index {idx}: got {actual:?}, expected {expected:?}"),
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
enum Expect<'a> {
Text(&'a str),
Bold(Vec<Self>),
Italic(Vec<Self>),
Code(&'a str),
SoftBreak,
HardBreak,
Link {
text: Vec<Self>,
url: &'a str,
title: Option<&'a str>,
},
Image {
alt: &'a str,
url: &'a str,
title: Option<&'a str>,
},
}
impl<'a> Expect<'a> {
fn text(s: &'a str) -> Vec<Self> {
vec![Self::Text(s)]
}
}
#[test]
fn test_heading() {
let md: MarkdownFile<'_> = MarkdownFile::parse("# Hello\n## World");
assert_eq!(md.sections.len(), 2);
match &md.sections[0] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &Expect::text("Hello"));
}
other => panic!("expected heading, got {other:?}"),
}
match &md.sections[1] {
Section::Heading { level: 2, content } => {
Checker::new(&md).check_span(*content, &Expect::text("World"));
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_paragraph() {
let md: MarkdownFile<'_> = MarkdownFile::parse("This is a paragraph.\nWith two lines.");
assert_eq!(md.sections.len(), 1);
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("This is a paragraph."),
Expect::SoftBreak,
Expect::Text("With two lines."),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_code_block() {
let md: MarkdownFile<'_> = MarkdownFile::parse("```rust\nfn main() {}\n```");
assert_eq!(md.sections.len(), 1);
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: Some("rust"),
code: "fn main() {}",
}
);
}
#[test]
fn test_code_block_no_language() {
let md: MarkdownFile<'_> = MarkdownFile::parse("```\nhello\n```");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: None,
code: "hello",
}
);
}
#[test]
fn test_unordered_list() {
let md: MarkdownFile<'_> = MarkdownFile::parse("- one\n- two\n- three");
assert_eq!(md.sections.len(), 1);
match &md.sections[0] {
Section::UnorderedList { items } => {
let items = &md[*items];
assert_eq!(items.len(), 3);
Checker::new(&md).check_span(items[0], &Expect::text("one"));
Checker::new(&md).check_span(items[1], &Expect::text("two"));
Checker::new(&md).check_span(items[2], &Expect::text("three"));
}
other => panic!("expected list, got {other:?}"),
}
}
#[test]
fn test_unordered_list_plus() {
let md: MarkdownFile<'_> = MarkdownFile::parse("+ one\n+ two\n+ three");
match &md.sections[0] {
Section::UnorderedList { items } => {
let items = &md[*items];
assert_eq!(items.len(), 3);
Checker::new(&md).check_span(items[0], &Expect::text("one"));
}
other => panic!("expected list, got {other:?}"),
}
}
#[test]
fn test_ordered_list() {
let md: MarkdownFile<'_> = MarkdownFile::parse("1. first\n2. second\n3. third");
match &md.sections[0] {
Section::OrderedList {
start: 1,
delimiter,
items,
} if *delimiter == OrderedListDelimiter::Dot => {
let items = &md[*items];
assert_eq!(items.len(), 3);
Checker::new(&md).check_span(items[0], &Expect::text("first"));
Checker::new(&md).check_span(items[1], &Expect::text("second"));
Checker::new(&md).check_span(items[2], &Expect::text("third"));
}
other => panic!("expected ordered list, got {other:?}"),
}
}
#[test]
fn test_blockquote() {
let md: MarkdownFile<'_> = MarkdownFile::parse("> line one\n> line two");
match &md.sections[0] {
Section::Blockquote { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("line one"),
Expect::Text("\n"),
Expect::Text("line two"),
],
),
other => panic!("expected blockquote, got {other:?}"),
}
}
#[test]
fn test_horizontal_rule() {
let md: MarkdownFile<'_> = MarkdownFile::parse("---");
assert_eq!(md.sections, vec![Section::HorizontalRule]);
}
#[test]
fn test_mixed_document() {
let md: MarkdownFile<'_> = MarkdownFile::parse(
"# Title\n\nSome text.\n\n- a\n- b\n\n> quote\n\n---\n\n```\ncode\n```",
);
assert_eq!(md.sections.len(), 6);
match &md.sections[0] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &Expect::text("Title"));
}
other => panic!("expected heading, got {other:?}"),
}
match &md.sections[1] {
Section::Paragraph { content } => {
Checker::new(&md).check_span(*content, &Expect::text("Some text."));
}
other => panic!("expected paragraph, got {other:?}"),
}
match &md.sections[2] {
Section::UnorderedList { items } => {
let items = &md[*items];
Checker::new(&md).check_span(items[0], &Expect::text("a"));
Checker::new(&md).check_span(items[1], &Expect::text("b"));
}
other => panic!("expected list, got {other:?}"),
}
match &md.sections[3] {
Section::Blockquote { content } => {
Checker::new(&md).check_span(*content, &Expect::text("quote"));
}
other => panic!("expected blockquote, got {other:?}"),
}
assert_eq!(md.sections[4], Section::HorizontalRule);
assert_eq!(
md.sections[5],
Section::CodeBlock {
language: None,
code: "code",
}
);
}
#[test]
fn test_heading_without_blank_line() {
let md: MarkdownFile<'_> = MarkdownFile::parse("some text\n# Heading");
assert_eq!(md.sections.len(), 2);
match &md.sections[0] {
Section::Paragraph { content } => {
Checker::new(&md).check_span(*content, &Expect::text("some text"));
}
other => panic!("expected paragraph, got {other:?}"),
}
match &md.sections[1] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &Expect::text("Heading"));
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_bold() {
let md: MarkdownFile<'_> = MarkdownFile::parse("This is **bold** text");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("This is "),
Expect::Bold(Expect::text("bold")),
Expect::Text(" text"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_italic() {
let md: MarkdownFile<'_> = MarkdownFile::parse("This is *italic* text");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("This is "),
Expect::Italic(Expect::text("italic")),
Expect::Text(" text"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_bold_underscore() {
let md: MarkdownFile<'_> = MarkdownFile::parse("__bold__");
match &md.sections[0] {
Section::Paragraph { content } => {
Checker::new(&md).check_span(*content, &[Expect::Bold(Expect::text("bold"))]);
}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_italic_underscore() {
let md: MarkdownFile<'_> = MarkdownFile::parse("_italic_");
match &md.sections[0] {
Section::Paragraph { content } => {
Checker::new(&md).check_span(*content, &[Expect::Italic(Expect::text("italic"))]);
}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_link() {
let md: MarkdownFile<'_> = MarkdownFile::parse("Click [here](https://example.com) now");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("Click "),
Expect::Link {
text: Expect::text("here"),
url: "https://example.com",
title: None,
},
Expect::Text(" now"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_image() {
let md: MarkdownFile<'_> = MarkdownFile::parse("");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[Expect::Image {
alt: "alt text",
url: "image.png",
title: None,
}],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_bold_inside_link() {
let md: MarkdownFile<'_> = MarkdownFile::parse("[**bold link**](url)");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[Expect::Link {
text: vec![Expect::Bold(Expect::text("bold link"))],
url: "url",
title: None,
}],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_inline_in_heading() {
let md: MarkdownFile<'_> = MarkdownFile::parse("# A **bold** heading");
match &md.sections[0] {
Section::Heading { level: 1, content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("A "),
Expect::Bold(Expect::text("bold")),
Expect::Text(" heading"),
],
),
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_inline_in_list() {
let md: MarkdownFile<'_> = MarkdownFile::parse("- *italic item*\n- **bold item**");
match &md.sections[0] {
Section::UnorderedList { items } => {
let items = &md[*items];
Checker::new(&md).check_span(items[0], &[Expect::Italic(Expect::text("italic item"))]);
Checker::new(&md).check_span(items[1], &[Expect::Bold(Expect::text("bold item"))]);
}
other => panic!("expected list, got {other:?}"),
}
}
#[test]
fn test_parse_readme_file() {
let content = std::fs::read_to_string("README.md").unwrap();
let md: MarkdownFile<'_> = MarkdownFile::parse(&content);
assert!(md.sections.len() > 10, "README should have many sections");
match &md.sections[0] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &Expect::text("marki"));
}
other => panic!("expected h1, got {other:?}"),
}
}
#[test]
fn test_normalize_lf_is_borrowed() {
let input = "hello\nworld";
let normalized = MarkdownFile::normalize(input);
assert!(matches!(normalized, Cow::Borrowed(_)));
assert_eq!(&*normalized, input);
}
#[test]
fn test_normalize_crlf_strips_cr() {
let input = "hello\r\nworld\r\n";
let normalized = MarkdownFile::normalize(input);
assert!(matches!(normalized, Cow::Owned(_)));
assert_eq!(&*normalized, "hello\nworld\n");
}
#[test]
fn test_crlf_paragraph() {
let input = MarkdownFile::normalize("line one\r\nline two\r\n");
let md: MarkdownFile<'_> = MarkdownFile::parse(&input);
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("line one"),
Expect::SoftBreak,
Expect::Text("line two"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_crlf_code_block() {
let input = MarkdownFile::normalize("```rust\r\nfn main() {}\r\nlet x = 1;\r\n```\r\n");
let md: MarkdownFile<'_> = MarkdownFile::parse(&input);
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: Some("rust"),
code: "fn main() {}\nlet x = 1;",
}
);
}
#[test]
fn test_crlf_mixed_document() {
let input = MarkdownFile::normalize("# Title\r\n\r\nSome text.\r\n\r\n- a\r\n- b\r\n");
let md: MarkdownFile<'_> = MarkdownFile::parse(&input);
assert_eq!(md.sections.len(), 3);
match &md.sections[0] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &Expect::text("Title"));
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_emphasis_backslash_space_no_close() {
let md: MarkdownFile<'_> = MarkdownFile::parse(r"*test\ *");
match &md.sections[0] {
Section::Paragraph { content } => {
Checker::new(&md).check_span(*content, &[Expect::Text(r"*test\ *")]);
}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_backslash_escape_punctuation() {
let md: MarkdownFile<'_> = MarkdownFile::parse(r"hello \*world\*");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("hello "),
Expect::Text("*world"),
Expect::Text("*"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_backslash_escape_non_punctuation() {
let md: MarkdownFile<'_> = MarkdownFile::parse(r"hello \n world");
match &md.sections[0] {
Section::Paragraph { content } => {
Checker::new(&md).check_span(*content, &[Expect::Text(r"hello \n world")]);
}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_ordered_list_paren_delimiter() {
let md: MarkdownFile<'_> = MarkdownFile::parse("1) first\n2) second");
match &md.sections[0] {
Section::OrderedList {
start: 1,
delimiter,
items,
} if *delimiter == OrderedListDelimiter::Paren => {
let items = &md[*items];
Checker::new(&md).check_span(items[0], &Expect::text("first"));
Checker::new(&md).check_span(items[1], &Expect::text("second"));
}
other => panic!("expected ordered list, got {other:?}"),
}
}
#[test]
fn test_ordered_list_different_delimiters_split() {
let md: MarkdownFile<'_> = MarkdownFile::parse("1. dot\n2) paren");
assert_eq!(md.sections.len(), 2);
match &md.sections[0] {
Section::OrderedList {
start: 1,
delimiter,
items,
} if *delimiter == OrderedListDelimiter::Dot => {
let items = &md[*items];
Checker::new(&md).check_span(items[0], &Expect::text("dot"));
}
other => panic!("expected ordered list dot, got {other:?}"),
}
match &md.sections[1] {
Section::OrderedList {
start: 2,
delimiter,
items,
} if *delimiter == OrderedListDelimiter::Paren => {
let items = &md[*items];
Checker::new(&md).check_span(items[0], &Expect::text("paren"));
}
other => panic!("expected ordered list paren, got {other:?}"),
}
}
#[test]
fn test_ordered_list_paren_custom_start() {
let md: MarkdownFile<'_> = MarkdownFile::parse("5) fifth\n6) sixth");
match &md.sections[0] {
Section::OrderedList {
start: 5,
delimiter,
items,
} if *delimiter == OrderedListDelimiter::Paren => {
let items = &md[*items];
Checker::new(&md).check_span(items[0], &Expect::text("fifth"));
Checker::new(&md).check_span(items[1], &Expect::text("sixth"));
}
other => panic!("expected ordered list, got {other:?}"),
}
}
#[test]
fn test_heading_closing_hashes() {
let md: MarkdownFile<'_> = MarkdownFile::parse("# Heading #");
match &md.sections[0] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &Expect::text("Heading"));
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_heading_closing_multiple_hashes() {
let md: MarkdownFile<'_> = MarkdownFile::parse("## Heading ##");
match &md.sections[0] {
Section::Heading { level: 2, content } => {
Checker::new(&md).check_span(*content, &Expect::text("Heading"));
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_heading_closing_mismatched_hashes() {
let md: MarkdownFile<'_> = MarkdownFile::parse("# Heading ####");
match &md.sections[0] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &Expect::text("Heading"));
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_heading_hash_no_space_not_stripped() {
let md: MarkdownFile<'_> = MarkdownFile::parse("# Heading#");
match &md.sections[0] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &Expect::text("Heading#"));
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_heading_only_hashes() {
let md: MarkdownFile<'_> = MarkdownFile::parse("# ###");
match &md.sections[0] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &[]);
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_soft_break_in_paragraph() {
let md: MarkdownFile<'_> = MarkdownFile::parse("line one\nline two\nline three");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("line one"),
Expect::SoftBreak,
Expect::Text("line two"),
Expect::SoftBreak,
Expect::Text("line three"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_soft_break_single_trailing_space() {
let md: MarkdownFile<'_> = MarkdownFile::parse("line one \nline two");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("line one "),
Expect::SoftBreak,
Expect::Text("line two"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_hard_break_two_trailing_spaces() {
let md: MarkdownFile<'_> = MarkdownFile::parse("line one \nline two");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("line one"),
Expect::HardBreak,
Expect::Text("line two"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_hard_break_many_trailing_spaces() {
let md: MarkdownFile<'_> = MarkdownFile::parse("line one \nline two");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("line one"),
Expect::HardBreak,
Expect::Text("line two"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_hard_break_trailing_backslash() {
let md: MarkdownFile<'_> = MarkdownFile::parse("line one\\\nline two");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("line one"),
Expect::HardBreak,
Expect::Text("line two"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_hard_break_with_inline() {
let md: MarkdownFile<'_> = MarkdownFile::parse("**bold** \nnext line");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Bold(Expect::text("bold")),
Expect::HardBreak,
Expect::Text("next line"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_blockquote_lazy_continuation() {
let md: MarkdownFile<'_> = MarkdownFile::parse("> line one\ncontinuation");
match &md.sections[0] {
Section::Blockquote { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("line one"),
Expect::Text("\n"),
Expect::Text("continuation"),
],
),
other => panic!("expected blockquote, got {other:?}"),
}
}
#[test]
fn test_blockquote_lazy_multiple_lines() {
let md: MarkdownFile<'_> = MarkdownFile::parse("> first\nsecond\nthird");
match &md.sections[0] {
Section::Blockquote { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("first"),
Expect::Text("\n"),
Expect::Text("second"),
Expect::Text("\n"),
Expect::Text("third"),
],
),
other => panic!("expected blockquote, got {other:?}"),
}
}
#[test]
fn test_blockquote_lazy_stops_at_heading() {
let md: MarkdownFile<'_> = MarkdownFile::parse("> quoted\n# Heading");
assert_eq!(md.sections.len(), 2);
match &md.sections[0] {
Section::Blockquote { content } => {
Checker::new(&md).check_span(*content, &Expect::text("quoted"));
}
other => panic!("expected blockquote, got {other:?}"),
}
match &md.sections[1] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &Expect::text("Heading"));
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_blockquote_lazy_stops_at_hr() {
let md: MarkdownFile<'_> = MarkdownFile::parse("> quoted\n---");
assert_eq!(md.sections.len(), 2);
match &md.sections[0] {
Section::Blockquote { content } => {
Checker::new(&md).check_span(*content, &Expect::text("quoted"));
}
other => panic!("expected blockquote, got {other:?}"),
}
assert_eq!(md.sections[1], Section::HorizontalRule);
}
#[test]
fn test_blockquote_lazy_stops_at_list() {
let md: MarkdownFile<'_> = MarkdownFile::parse("> quoted\n- item");
assert_eq!(md.sections.len(), 2);
match &md.sections[0] {
Section::Blockquote { content } => {
Checker::new(&md).check_span(*content, &Expect::text("quoted"));
}
other => panic!("expected blockquote, got {other:?}"),
}
match &md.sections[1] {
Section::UnorderedList { items } => {
let items = &md[*items];
Checker::new(&md).check_span(items[0], &Expect::text("item"));
}
other => panic!("expected list, got {other:?}"),
}
}
#[test]
fn test_blockquote_lazy_stops_at_code_fence() {
let md: MarkdownFile<'_> = MarkdownFile::parse("> quoted\n```\ncode\n```");
assert_eq!(md.sections.len(), 2);
match &md.sections[0] {
Section::Blockquote { content } => {
Checker::new(&md).check_span(*content, &Expect::text("quoted"));
}
other => panic!("expected blockquote, got {other:?}"),
}
assert_eq!(
md.sections[1],
Section::CodeBlock {
language: None,
code: "code",
}
);
}
#[test]
fn test_link_with_double_quote_title() {
let md: MarkdownFile<'_> = MarkdownFile::parse(r#"[text](url "a title")"#);
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[Expect::Link {
text: Expect::text("text"),
url: "url",
title: Some("a title"),
}],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_link_with_single_quote_title() {
let md: MarkdownFile<'_> = MarkdownFile::parse("[text](url 'a title')");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[Expect::Link {
text: Expect::text("text"),
url: "url",
title: Some("a title"),
}],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_link_with_paren_title() {
let md: MarkdownFile<'_> = MarkdownFile::parse("[text](url (a title))");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[Expect::Link {
text: Expect::text("text"),
url: "url",
title: Some("a title"),
}],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_link_no_title() {
let md: MarkdownFile<'_> = MarkdownFile::parse("[text](url)");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[Expect::Link {
text: Expect::text("text"),
url: "url",
title: None,
}],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_image_with_title() {
let md: MarkdownFile<'_> = MarkdownFile::parse(r#""#);
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[Expect::Image {
alt: "alt",
url: "img.png",
title: Some("photo"),
}],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_emphasis_star_intraword() {
let md: MarkdownFile<'_> = MarkdownFile::parse("foo*bar*baz");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("foo"),
Expect::Italic(Expect::text("bar")),
Expect::Text("baz"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_emphasis_underscore_no_intraword() {
let md: MarkdownFile<'_> = MarkdownFile::parse("foo_bar_baz");
match &md.sections[0] {
Section::Paragraph { content } => {
Checker::new(&md).check_span(*content, &Expect::text("foo_bar_baz"));
}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_emphasis_underscore_word_boundaries() {
let md: MarkdownFile<'_> = MarkdownFile::parse("_foo_ bar _baz_");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Italic(Expect::text("foo")),
Expect::Text(" bar "),
Expect::Italic(Expect::text("baz")),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_bold_underscore_no_intraword() {
let md: MarkdownFile<'_> = MarkdownFile::parse("foo__bar__baz");
match &md.sections[0] {
Section::Paragraph { content } => {
Checker::new(&md).check_span(*content, &Expect::text("foo__bar__baz"));
}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_emphasis_star_after_punctuation() {
let md: MarkdownFile<'_> = MarkdownFile::parse("(*foo*)");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("("),
Expect::Italic(Expect::text("foo")),
Expect::Text(")"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_emphasis_underscore_after_punctuation() {
let md: MarkdownFile<'_> = MarkdownFile::parse("(_foo_)");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("("),
Expect::Italic(Expect::text("foo")),
Expect::Text(")"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_emphasis_not_opened_by_whitespace_after() {
let md: MarkdownFile<'_> = MarkdownFile::parse("a * not emphasis * b");
match &md.sections[0] {
Section::Paragraph { content } => {
Checker::new(&md).check_span(*content, &Expect::text("a * not emphasis * b"));
}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_bold_star_intraword() {
let md: MarkdownFile<'_> = MarkdownFile::parse("foo**bar**baz");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("foo"),
Expect::Bold(Expect::text("bar")),
Expect::Text("baz"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_triple_star_bold_italic() {
let md: MarkdownFile<'_> = MarkdownFile::parse("***bold italic***");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[Expect::Italic(vec![Expect::Bold(Expect::text(
"bold italic",
))])],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_triple_star_bold_italic_intraword() {
let md: MarkdownFile<'_> = MarkdownFile::parse("foo***bar***baz");
match &md.sections[0] {
Section::Paragraph { content } => Checker::new(&md).check_span(
*content,
&[
Expect::Text("foo"),
Expect::Italic(vec![Expect::Bold(Expect::text("bar"))]),
Expect::Text("baz"),
],
),
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_heading_indented_1_space() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" # Hello");
match &md.sections[0] {
Section::Heading { level: 1, content } => {
Checker::new(&md).check_span(*content, &Expect::text("Hello"));
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_heading_indented_3_spaces() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" ## World");
match &md.sections[0] {
Section::Heading { level: 2, content } => {
Checker::new(&md).check_span(*content, &Expect::text("World"));
}
other => panic!("expected heading, got {other:?}"),
}
}
#[test]
fn test_heading_indented_4_spaces_is_paragraph() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" # Not a heading");
match &md.sections[0] {
Section::Paragraph { .. } => {}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_hr_indented_3_spaces() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" ---");
assert_eq!(md.sections, vec![Section::HorizontalRule]);
}
#[test]
fn test_hr_indented_4_spaces_is_paragraph() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" ---");
match &md.sections[0] {
Section::Paragraph { .. } => {}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_unordered_list_indented_2_spaces() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" - one\n - two");
match &md.sections[0] {
Section::UnorderedList { items } => {
let items = &md[*items];
assert_eq!(items.len(), 2);
Checker::new(&md).check_span(items[0], &Expect::text("one"));
Checker::new(&md).check_span(items[1], &Expect::text("two"));
}
other => panic!("expected list, got {other:?}"),
}
}
#[test]
fn test_unordered_list_indented_4_spaces_is_paragraph() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" - not a list");
match &md.sections[0] {
Section::Paragraph { .. } => {}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_ordered_list_indented_1_space() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" 1. first\n 2. second");
match &md.sections[0] {
Section::OrderedList {
start: 1, items, ..
} => {
let items = &md[*items];
assert_eq!(items.len(), 2);
Checker::new(&md).check_span(items[0], &Expect::text("first"));
}
other => panic!("expected ordered list, got {other:?}"),
}
}
#[test]
fn test_blockquote_indented_3_spaces() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" > quoted");
match &md.sections[0] {
Section::Blockquote { content } => {
Checker::new(&md).check_span(*content, &Expect::text("quoted"));
}
other => panic!("expected blockquote, got {other:?}"),
}
}
#[test]
fn test_blockquote_indented_4_spaces_is_paragraph() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" > not a quote");
match &md.sections[0] {
Section::Paragraph { .. } => {}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_code_fence_indented_3_spaces() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" ```\nhello\n ```");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: None,
code: "hello",
}
);
}
#[test]
fn test_code_fence_indented_4_spaces_is_paragraph() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" ```\nnot code\n ```");
match &md.sections[0] {
Section::Paragraph { .. } => {}
other => panic!("expected paragraph, got {other:?}"),
}
}
#[test]
fn test_tilde_code_fence() {
let md: MarkdownFile<'_> = MarkdownFile::parse("~~~\nhello\n~~~");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: None,
code: "hello",
}
);
}
#[test]
fn test_tilde_code_fence_with_language() {
let md: MarkdownFile<'_> = MarkdownFile::parse("~~~python\nprint('hi')\n~~~");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: Some("python"),
code: "print('hi')",
}
);
}
#[test]
fn test_tilde_code_fence_longer_close() {
let md: MarkdownFile<'_> = MarkdownFile::parse("~~~\nhello\n~~~~~");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: None,
code: "hello",
}
);
}
#[test]
fn test_tilde_fence_not_closed_by_backticks() {
let md: MarkdownFile<'_> = MarkdownFile::parse("~~~\nhello\n```\n~~~");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: None,
code: "hello\n```",
}
);
}
#[test]
fn test_backtick_fence_not_closed_by_tildes() {
let md: MarkdownFile<'_> = MarkdownFile::parse("```\nhello\n~~~\n```");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: None,
code: "hello\n~~~",
}
);
}
#[test]
fn test_tilde_fence_backticks_in_info_string() {
let md: MarkdownFile<'_> = MarkdownFile::parse("~~~ aa ```\nhello\n~~~");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: Some("aa ```"),
code: "hello",
}
);
}
#[test]
fn test_tilde_fence_indented() {
let md: MarkdownFile<'_> = MarkdownFile::parse(" ~~~\nhello\n ~~~");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: None,
code: "hello",
}
);
}
#[test]
fn test_ordered_list_empty_item() {
let md: MarkdownFile<'_> = MarkdownFile::parse("1. \n2. second");
match &md.sections[0] {
Section::OrderedList {
start: 1, items, ..
} => {
let items = &md[*items];
assert_eq!(items.len(), 2);
Checker::new(&md).check_span(items[0], &[]);
Checker::new(&md).check_span(items[1], &Expect::text("second"));
}
other => panic!("expected ordered list, got {other:?}"),
}
}
#[test]
fn test_normalize_bare_cr_becomes_newline() {
let input = "hello\rworld";
let normalized = MarkdownFile::normalize(input);
assert_eq!(&*normalized, "hello\nworld");
}
#[test]
fn test_normalize_mixed_cr_crlf() {
let input = "a\rb\r\nc\r";
let normalized = MarkdownFile::normalize(input);
assert_eq!(&*normalized, "a\nb\nc\n");
}
#[test]
fn test_closing_fence_indented_3_spaces() {
let md: MarkdownFile<'_> = MarkdownFile::parse("```\nhello\n ```");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: None,
code: "hello",
}
);
}
#[test]
fn test_closing_fence_indented_4_spaces_not_closing() {
let md: MarkdownFile<'_> = MarkdownFile::parse("```\nhello\n ```");
assert_eq!(
md.sections[0],
Section::CodeBlock {
language: None,
code: "hello\n ```",
}
);
}