use crate::elements::*;
use std::collections::HashMap;
pub fn parse_document(markdown: &str) -> Vec<MarkdownElement> {
let lines: Vec<&str> = markdown.lines().collect();
let mut elements: Vec<MarkdownElement> = Vec::new();
let mut refs: HashMap<String, String> = HashMap::new();
let mut footnotes: Vec<FootnoteDef> = Vec::new();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim();
if trimmed.is_empty() {
elements.push(MarkdownElement::BlankLine);
i += 1;
continue;
}
if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
let fence_char = &trimmed[..1];
let language = trimmed[3..].trim();
let language = if language.is_empty() { None } else { Some(language.to_string()) };
let mut code_lines = Vec::new();
i += 1;
while i < lines.len() {
let l = lines[i];
if l.trim().starts_with(fence_char) && l.trim().len() >= 3
&& l.trim().chars().take(3).all(|c| c.to_string() == *fence_char)
{
i += 1;
break;
}
code_lines.push(l.to_string());
i += 1;
}
elements.push(MarkdownElement::CodeBlock {
language,
lines: code_lines,
});
continue;
}
if is_horizontal_rule(trimmed) {
elements.push(MarkdownElement::HorizontalRule);
i += 1;
continue;
}
if i + 1 < lines.len() {
let next = lines[i + 1].trim();
if next.chars().all(|c| c == '=') && next.len() >= 3 {
elements.push(MarkdownElement::Heading {
level: 1,
text: trimmed.to_string(),
});
i += 2;
continue;
}
if next.chars().all(|c| c == '-') && next.len() >= 3 {
elements.push(MarkdownElement::Heading {
level: 2,
text: trimmed.to_string(),
});
i += 2;
continue;
}
}
if trimmed.starts_with('#') {
let level = trimmed.chars().take_while(|&c| c == '#').count() as u8;
if level <= 6 && trimmed.len() > level as usize && trimmed.as_bytes()[level as usize] == b' ' {
let text = trimmed[level as usize..].trim().to_string();
elements.push(MarkdownElement::Heading { level, text });
i += 1;
continue;
}
}
if trimmed.starts_with('>') {
let mut quoted_lines = Vec::new();
while i < lines.len() {
let l = lines[i];
if !l.trim().starts_with('>') {
break;
}
let content = l.trim().strip_prefix('>').unwrap_or(l).trim();
quoted_lines.push(content.to_string());
i += 1;
}
let text = quoted_lines.join(" ");
elements.push(MarkdownElement::Blockquote { text });
continue;
}
if trimmed.starts_with('|') && trimmed.ends_with('|')
&& let Some(table) = try_parse_table(&lines, i) {
let row_count = table.rows.len();
elements.push(MarkdownElement::Table(table));
i += row_count + 2;
continue;
}
if is_task_list_item(trimmed) {
let mut items = Vec::new();
while i < lines.len() {
let l = lines[i];
let t = l.trim();
if !is_task_list_item(t) { break; }
let (checked, text) = parse_task_item(t);
items.push(TaskItem { checked, text: text.to_string() });
i += 1;
}
elements.push(MarkdownElement::TaskList { items, depth: 0 });
continue;
}
if is_unordered_list_item(trimmed) {
let mut items = Vec::new();
while i < lines.len() {
let l = lines[i];
if !is_unordered_list_item(l.trim()) {
break;
}
items.push(strip_list_marker(l.trim()).to_string());
i += 1;
}
elements.push(MarkdownElement::UnorderedList { items, depth: 0 });
while i < lines.len() && is_unordered_list_item_nested(lines[i]) {
let mut nitems = Vec::new();
while i < lines.len() {
let l = lines[i];
if !is_unordered_list_item_nested(l) { break; }
nitems.push(strip_list_marker(&l[4..]).to_string());
i += 1;
}
elements.push(MarkdownElement::UnorderedList { items: nitems, depth: 1 });
}
if i < lines.len()
&& let Some(_start) = is_ordered_list_item_nested(lines[i]) {
let mut nitems = Vec::new();
while i < lines.len() {
let l = lines[i];
if is_ordered_list_item_nested(l).is_none() { break; }
nitems.push(strip_list_marker(&l[4..]).to_string());
i += 1;
}
elements.push(MarkdownElement::OrderedList { items: nitems, start: 1, depth: 1 });
}
continue;
}
if let Some(start) = is_ordered_list_item(trimmed) {
let mut items = Vec::new();
while i < lines.len() {
let l = lines[i];
if is_ordered_list_item(l.trim()).is_none() {
break;
}
items.push(strip_list_marker(l.trim()).to_string());
i += 1;
}
elements.push(MarkdownElement::OrderedList { items, start, depth: 0 });
while i < lines.len() && is_unordered_list_item_nested(lines[i]) {
let mut nitems = Vec::new();
while i < lines.len() {
let l = lines[i];
if !is_unordered_list_item_nested(l) { break; }
nitems.push(strip_list_marker(&l[4..]).to_string());
i += 1;
}
elements.push(MarkdownElement::UnorderedList { items: nitems, depth: 1 });
}
if i < lines.len()
&& let Some(_start) = is_ordered_list_item_nested(lines[i]) {
let mut nitems = Vec::new();
while i < lines.len() {
let l = lines[i];
if is_ordered_list_item_nested(l).is_none() { break; }
nitems.push(strip_list_marker(&l[4..]).to_string());
i += 1;
}
elements.push(MarkdownElement::OrderedList { items: nitems, start: 1, depth: 1 });
}
continue;
}
if let Some(tag) = is_html_block_start(trimmed) {
let mut html_lines = vec![trimmed.to_string()];
i += 1;
let close = format!("</{}>", tag);
while i < lines.len() {
let l = lines[i];
html_lines.push(l.to_string());
if l.to_lowercase().contains(&close) {
i += 1;
break;
}
if l.trim().is_empty() { i += 1; break; }
i += 1;
}
elements.push(MarkdownElement::HtmlBlock { lines: html_lines });
continue;
}
if line.starts_with(" ") || (line.starts_with('\t') && line.len() > 1) {
let mut code_lines = Vec::new();
while i < lines.len() {
let l = lines[i];
if let Some(stripped) = l.strip_prefix(" ") {
code_lines.push(stripped.to_string());
} else if let Some(stripped) = l.strip_prefix('\t') {
code_lines.push(stripped.to_string());
} else if l.is_empty() {
code_lines.push(String::new());
} else {
break;
}
i += 1;
}
elements.push(MarkdownElement::CodeBlock {
language: None,
lines: code_lines,
});
continue;
}
if let Some((label, url)) = try_parse_refdef(trimmed) {
refs.insert(label, url);
i += 1;
continue;
}
if let Some((id, text)) = is_footnote_def(trimmed) {
let mut fn_text = text;
let mut first = true;
i += 1;
while i < lines.len() && (lines[i].trim().starts_with(" ") || (lines[i].trim().is_empty() && i + 1 < lines.len() && lines[i + 1].trim().starts_with(" "))) {
if lines[i].trim().is_empty() && first { i += 1; continue; }
first = false;
fn_text.push(' ');
fn_text.push_str(lines[i].trim().trim_start());
i += 1;
}
footnotes.push(FootnoteDef { id, text: fn_text });
continue;
}
if is_definition_list_term(trimmed) && i + 1 < lines.len() && lines[i + 1].trim().starts_with(": ") {
let term = trimmed.to_string();
let mut defs = Vec::new();
i += 1;
while i < lines.len() {
let l = lines[i];
if l.trim().starts_with(": ") {
defs.push(l.trim()[2..].to_string());
i += 1;
} else if l.trim().is_empty() { i += 1; break; }
else { break; }
}
elements.push(MarkdownElement::DefinitionList { items: vec![(term, defs)] });
continue;
}
let mut para_lines = Vec::new();
while i < lines.len() {
let l = lines[i];
if l.trim().is_empty()
|| l.trim().starts_with('#')
|| l.trim().starts_with('>')
|| l.trim().starts_with("```")
|| l.trim().starts_with("~~~")
|| is_horizontal_rule(l.trim())
|| (l.trim().starts_with('|') && l.trim().ends_with('|'))
|| is_unordered_list_item(l.trim())
|| is_ordered_list_item(l.trim()).is_some()
|| is_setext_underline(&lines, i)
|| l.starts_with(" ")
|| (l.starts_with('\t') && l.len() > 1)
{
break;
}
para_lines.push(l.trim().to_string());
i += 1;
}
if !para_lines.is_empty() {
elements.push(MarkdownElement::Paragraph {
text: para_lines.join(" "),
});
continue;
}
i += 1;
}
resolve_references(&mut elements, &refs);
if !footnotes.is_empty() {
elements.push(MarkdownElement::FootnoteSection { items: footnotes });
}
elements
}
fn resolve_references(elements: &mut [MarkdownElement], refs: &HashMap<String, String>) {
if refs.is_empty() {
return;
}
for elem in elements.iter_mut() {
let text: &mut String = match elem {
MarkdownElement::Heading { text, .. } => text,
MarkdownElement::Paragraph { text } => text,
MarkdownElement::Blockquote { text } => text,
MarkdownElement::OrderedList { items, .. } => {
for item in items { resolve_refs_in_text(item, refs); }
continue;
}
MarkdownElement::UnorderedList { items, .. } => {
for item in items { resolve_refs_in_text(item, refs); }
continue;
}
MarkdownElement::CodeBlock { .. }
| MarkdownElement::Table(..)
| MarkdownElement::HtmlBlock { .. }
| MarkdownElement::TaskList { .. }
| MarkdownElement::FootnoteSection { .. }
| MarkdownElement::DefinitionList { .. }
| MarkdownElement::HorizontalRule
| MarkdownElement::BlankLine => continue,
};
resolve_refs_in_text(text, refs);
}
}
fn resolve_refs_in_text(text: &mut String, refs: &HashMap<String, String>) {
let chars: Vec<char> = text.chars().collect();
let mut result = String::new();
let mut i = 0;
while i < chars.len() {
if chars[i] == '['
&& let Some(end_b) = find_char(&chars, i + 1, ']') {
let inner: String = chars[i + 1..end_b].iter().collect();
if inner.contains('[') || inner.contains('(') {
result.push('[');
i += 1;
continue;
}
if end_b + 1 < chars.len() && chars[end_b + 1] == '['
&& let Some(end_r) = find_char(&chars, end_b + 2, ']') {
let ref_label: String = chars[end_b + 2..end_r].iter().collect();
let label = if ref_label.is_empty() { &inner } else { &ref_label };
let label_norm = label.to_lowercase();
if let Some(url) = refs.get(&label_norm) {
result.push_str(&format!("[{inner}]({url})"));
i = end_r + 1;
continue;
}
}
result.push('[');
i += 1;
continue;
}
result.push(chars[i]);
i += 1;
}
*text = result;
}
fn try_parse_refdef(line: &str) -> Option<(String, String)> {
if !line.starts_with('[') {
return None;
}
if let Some(b) = line.find("]:") {
let label = line[1..b].trim().to_lowercase();
if label.is_empty() || label.contains('\n') {
return None;
}
let rest = line[b + 2..].trim();
if rest.is_empty() {
return None;
}
let url = rest.split_whitespace().next().unwrap_or("");
if url.starts_with('<') && url.ends_with('>') {
return Some((label, url[1..url.len() - 1].to_string()));
}
Some((label, url.to_string()))
} else {
None
}
}
fn find_char(chars: &[char], start: usize, c: char) -> Option<usize> {
for (i, &ch) in chars.iter().enumerate().skip(start) {
if ch == c && (i == 0 || chars[i - 1] != '\\') {
return Some(i);
}
}
None
}
fn is_horizontal_rule(line: &str) -> bool {
let cleaned: String = line.chars().filter(|&c| c != ' ').collect();
if cleaned.len() < 3 {
return false;
}
let all_same = |c: char| cleaned.chars().all(|ch| ch == c);
all_same('-') || all_same('*') || all_same('_')
}
fn is_task_list_item(line: &str) -> bool {
line.starts_with("- [ ] ") || line.starts_with("- [x] ") || line.starts_with("- [X] ")
|| line.starts_with("* [ ] ") || line.starts_with("* [x] ") || line.starts_with("* [X] ")
}
fn parse_task_item(line: &str) -> (bool, &str) {
let idx = line.find("] ").unwrap_or(0);
let checked = line[..idx].contains("[x]") || line[..idx].contains("[X]");
(checked, line[idx + 2..].trim())
}
fn is_footnote_def(line: &str) -> Option<(String, String)> {
if !line.starts_with("[^") { return None; }
let close = line.find("]:")?;
if close <= 2 { return None; }
let id = line[2..close].to_string();
let text = line[close + 2..].trim().to_string();
Some((id, text))
}
fn is_definition_list_term(line: &str) -> bool {
if line.starts_with('#') || line.starts_with('>') || line.starts_with('|')
|| line.starts_with('-') || line.starts_with('*') || line.starts_with('`')
|| line.is_empty() || is_horizontal_rule(line) {
return false;
}
true
}
fn is_html_block_start(line: &str) -> Option<String> {
if !line.starts_with('<') {
return None;
}
let rest = &line[1..];
let tag_end = rest.find(|c: char| c == '>' || c.is_whitespace())?;
let tag = &rest[..tag_end];
let lower = tag.to_lowercase();
match lower.as_str() {
"div" | "pre" | "table" | "script" | "style" | "section"
| "article" | "nav" | "footer" | "header" | "aside" | "main"
| "blockquote" | "form" | "fieldset" | "details" | "dialog"
| "figure" | "figcaption" | "dl" | "ol" | "ul" | "h1" | "h2"
| "h3" | "h4" | "h5" | "h6" => Some(tag.to_string()),
_ => None,
}
}
fn is_setext_underline(lines: &[&str], idx: usize) -> bool {
if idx == 0 || idx >= lines.len() {
return false;
}
let prev = lines[idx - 1].trim();
if prev.is_empty() || prev.starts_with('#') || prev.starts_with('>') || prev.starts_with('|') {
return false;
}
let cur = lines[idx].trim();
(cur.chars().all(|c| c == '=') && cur.len() >= 3)
|| (cur.chars().all(|c| c == '-') && cur.len() >= 3)
}
fn is_unordered_list_item(line: &str) -> bool {
line.starts_with("* ") || line.starts_with("- ") || line.starts_with("+ ")
}
fn is_unordered_list_item_nested(line: &str) -> bool {
line.starts_with(" * ") || line.starts_with(" - ") || line.starts_with(" + ")
}
fn is_ordered_list_item_nested(line: &str) -> Option<u64> {
let dot_pos = line.find(". ")?;
if dot_pos < 4 { return None; }
let num_part = line[4..dot_pos].trim();
num_part.parse::<u64>().ok()
}
fn is_ordered_list_item(line: &str) -> Option<u64> {
let dot_pos = line.find(". ")?;
let num_part = &line[..dot_pos];
num_part.parse::<u64>().ok()
}
fn strip_list_marker(line: &str) -> &str {
if let Some(pos) = line.find(". ") {
return line[pos + 2..].trim();
}
if line.len() >= 2 {
return line[2..].trim();
}
line
}
fn try_parse_table(lines: &[&str], start: usize) -> Option<TableDef> {
if start + 1 >= lines.len() {
return None;
}
let header_line = lines[start].trim();
let sep_line = lines[start + 1].trim();
if !sep_line.starts_with('|') || !sep_line.ends_with('|') {
return None;
}
let is_sep = sep_line
.chars()
.filter(|&c| c != ' ' && c != '|' && c != '-' && c != ':')
.count()
== 0;
if !is_sep {
return None;
}
let headers = split_row(header_line);
let seps = split_row(sep_line);
let num_cols = headers.len();
let alignments: Vec<Alignment> = seps.iter().map(|s| parse_alignment(s)).collect();
let mut rows: Vec<Vec<String>> = Vec::new();
let mut i = start + 2;
while i < lines.len() {
let trimmed = lines[i].trim();
if trimmed.is_empty() {
break;
}
if !trimmed.starts_with('|') || !trimmed.ends_with('|') {
break;
}
let cells = split_row(trimmed);
if cells.len() != num_cols {
break;
}
let row: Vec<String> = cells.into_iter().map(|c| c.trim().to_string()).collect();
rows.push(row);
i += 1;
}
Some(TableDef {
headers,
alignments,
rows,
})
}
fn split_row(line: &str) -> Vec<String> {
let inner = line
.strip_prefix('|')
.and_then(|s| s.strip_suffix('|'))
.unwrap_or(line);
let mut cells = Vec::new();
let mut current = String::new();
let mut escaping = false;
for ch in inner.chars() {
if escaping {
current.push(ch);
escaping = false;
} else if ch == '\\' {
escaping = true;
} else if ch == '|' {
cells.push(std::mem::take(&mut current));
} else {
current.push(ch);
}
}
cells.push(current);
cells
}
fn parse_alignment(sep: &str) -> Alignment {
let trimmed = sep.trim();
let starts = trimmed.starts_with(':');
let ends = trimmed.ends_with(':');
match (starts, ends) {
(true, true) => Alignment::Center,
(false, true) => Alignment::Right,
_ => Alignment::Left,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hr_dashes() {
assert!(is_horizontal_rule("---"));
}
#[test]
fn hr_dashes_with_spaces() {
assert!(is_horizontal_rule("- - -"));
}
#[test]
fn hr_asterisks() {
assert!(is_horizontal_rule("***"));
}
#[test]
fn hr_underscores() {
assert!(is_horizontal_rule("___"));
}
#[test]
fn hr_not_enough_chars() {
assert!(!is_horizontal_rule("--"));
}
#[test]
fn hr_mixed_chars() {
assert!(!is_horizontal_rule("-*-"));
}
#[test]
fn unordered_dash() {
assert!(is_unordered_list_item("- item"));
}
#[test]
fn unordered_star() {
assert!(is_unordered_list_item("* item"));
}
#[test]
fn unordered_plus() {
assert!(is_unordered_list_item("+ item"));
}
#[test]
fn unordered_no_space() {
assert!(!is_unordered_list_item("-item"));
}
#[test]
fn ordered_simple() {
assert_eq!(is_ordered_list_item("1. item"), Some(1));
}
#[test]
fn ordered_large_number() {
assert_eq!(is_ordered_list_item("999. item"), Some(999));
}
#[test]
fn ordered_no_dot() {
assert_eq!(is_ordered_list_item("1 item"), None);
}
#[test]
fn ordered_letters() {
assert_eq!(is_ordered_list_item("a. item"), None);
}
#[test]
fn strip_ordered_marker() {
assert_eq!(strip_list_marker("1. hello"), "hello");
}
#[test]
fn strip_ordered_long_number() {
assert_eq!(strip_list_marker("123. hello"), "hello");
}
#[test]
fn strip_unordered_marker() {
assert_eq!(strip_list_marker("- hello"), "hello");
}
#[test]
fn strip_unordered_star_marker() {
assert_eq!(strip_list_marker("* hello"), "hello");
}
#[test]
fn align_left_default() {
assert_eq!(parse_alignment("---"), Alignment::Left);
}
#[test]
fn align_left_explicit() {
assert_eq!(parse_alignment(":---"), Alignment::Left);
}
#[test]
fn align_center() {
assert_eq!(parse_alignment(":---:"), Alignment::Center);
}
#[test]
fn align_right() {
assert_eq!(parse_alignment("---:"), Alignment::Right);
}
#[test]
fn split_simple() {
assert_eq!(split_row("| a | b |"), vec![" a ", " b "]);
}
#[test]
fn split_with_whitespace() {
assert_eq!(split_row("| a | b |"), vec![" a ", " b "]);
}
#[test]
fn split_no_leading_trailing_pipes() {
assert_eq!(split_row("a | b"), vec!["a ", " b"]);
}
#[test]
fn split_empty_cells() {
assert_eq!(split_row("| | |"), vec![" ", " "]);
}
#[test]
fn try_parse_simple_table() {
let lines = vec!["| a | b |", "|---|---|", "| 1 | 2 |"];
let table = try_parse_table(&lines, 0).unwrap();
assert_eq!(table.headers, vec![" a ", " b "]);
assert_eq!(table.rows, vec![vec!["1".to_string(), "2".to_string()]]);
}
#[test]
fn split_escaped_pipe() {
let cells = split_row("| a \\| b | c |");
assert_eq!(cells.len(), 2);
assert!(cells[0].contains('|'), "escaped pipe should appear as literal |");
}
#[test]
fn try_parse_table_alignment() {
let lines = vec!["| a | b |", "|:---|---:|"];
let table = try_parse_table(&lines, 0).unwrap();
assert_eq!(table.alignments, vec![Alignment::Left, Alignment::Right]);
}
#[test]
fn try_parse_not_a_table() {
let lines = vec!["not a table"];
assert!(try_parse_table(&lines, 0).is_none());
}
#[test]
fn try_parse_table_no_separator() {
let lines = vec!["| a | b |", "not a separator"];
assert!(try_parse_table(&lines, 0).is_none());
}
#[test]
fn parse_doc_setext_h1() {
let elems = parse_document("Title\n=====\n");
assert_eq!(elems.len(), 1);
match &elems[0] {
MarkdownElement::Heading { level, text } => {
assert_eq!(*level, 1);
assert_eq!(text, "Title");
}
_ => panic!("expected heading"),
}
}
#[test]
fn parse_doc_setext_h2() {
let elems = parse_document("Title\n-----\n");
match &elems[0] {
MarkdownElement::Heading { level, text } => {
assert_eq!(*level, 2);
assert_eq!(text, "Title");
}
_ => panic!("expected heading"),
}
}
#[test]
fn parse_doc_atx_h3() {
let elems = parse_document("### Title\n");
match &elems[0] {
MarkdownElement::Heading { level, text } => {
assert_eq!(*level, 3);
assert_eq!(text, "Title");
}
_ => panic!("expected heading"),
}
}
#[test]
fn parse_doc_code_block_with_lang() {
let elems = parse_document("```rust\nlet x = 1;\n```\n");
match &elems[0] {
MarkdownElement::CodeBlock { language, lines } => {
assert_eq!(language.as_deref(), Some("rust"));
assert_eq!(lines, &vec!["let x = 1;"]);
}
_ => panic!("expected code block"),
}
}
#[test]
fn parse_doc_code_block_no_lang() {
let elems = parse_document("```\nhello\n```\n");
match &elems[0] {
MarkdownElement::CodeBlock { language, lines } => {
assert!(language.is_none());
assert_eq!(lines, &vec!["hello"]);
}
_ => panic!("expected code block"),
}
}
#[test]
fn parse_doc_code_block_tilde() {
let elems = parse_document("~~~bash\necho hi\n~~~\n");
match &elems[0] {
MarkdownElement::CodeBlock { language, lines } => {
assert_eq!(language.as_deref(), Some("bash"));
assert_eq!(lines, &vec!["echo hi"]);
}
_ => panic!("expected code block"),
}
}
#[test]
fn parse_doc_blockquote() {
let elems = parse_document("> quoted\n> more quoted\n");
match &elems[0] {
MarkdownElement::Blockquote { text } => {
assert!(text.contains("quoted"));
assert!(text.contains("more quoted"));
}
_ => panic!("expected blockquote"),
}
}
#[test]
fn parse_doc_unordered_list() {
let elems = parse_document("- one\n- two\n- three\n");
match &elems[0] {
MarkdownElement::UnorderedList { items, .. } => {
assert_eq!(items.len(), 3);
assert_eq!(items[0], "one");
}
_ => panic!("expected unordered list"),
}
}
#[test]
fn parse_doc_ordered_list() {
let elems = parse_document("1. one\n2. two\n");
match &elems[0] {
MarkdownElement::OrderedList { items, start, .. } => {
assert_eq!(*start, 1);
assert_eq!(items.len(), 2);
}
_ => panic!("expected ordered list"),
}
}
#[test]
fn parse_doc_horizontal_rule() {
let elems = parse_document("---\n");
assert!(matches!(elems[0], MarkdownElement::HorizontalRule));
}
#[test]
fn parse_doc_blank_line() {
let elems = parse_document("\n");
assert!(matches!(elems[0], MarkdownElement::BlankLine));
}
#[test]
fn parse_doc_table_and_paragraph() {
let markdown = "Some text\n\n| a | b |\n|---|---|\n| 1 | 2 |\n";
let elems = parse_document(markdown);
assert!(matches!(elems[0], MarkdownElement::Paragraph { .. }));
assert!(matches!(elems[2], MarkdownElement::Table(..)));
}
#[test]
fn parse_doc_multiline_paragraph() {
let elems = parse_document("line one\nline two\n");
match &elems[0] {
MarkdownElement::Paragraph { text } => {
assert!(text.contains("line one"));
assert!(text.contains("line two"));
}
_ => panic!("expected paragraph"),
}
}
#[test]
fn is_setext_underline_valid() {
let lines: Vec<&str> = vec!["Title", "===", "text"];
assert!(is_setext_underline(&lines, 1));
}
#[test]
fn is_setext_underline_not_first_line() {
let lines: Vec<&str> = vec!["====", ""];
assert!(!is_setext_underline(&lines, 0));
}
#[test]
fn is_setext_underline_prev_is_heading() {
let lines: Vec<&str> = vec!["# Heading", "===", ""];
assert!(!is_setext_underline(&lines, 1));
}
#[test]
fn is_setext_underline_short() {
let lines: Vec<&str> = vec!["Title", "==", ""];
assert!(!is_setext_underline(&lines, 1));
}
#[test]
fn parse_doc_indented_code_block() {
let elems = parse_document(" code line 1\n code line 2\n");
match &elems[0] {
MarkdownElement::CodeBlock { language, lines } => {
assert!(language.is_none());
assert_eq!(lines, &["code line 1", "code line 2"]);
}
_ => panic!("expected code block, got {:?}", elems),
}
}
#[test]
fn parse_doc_indented_code_block_tab() {
let elems = parse_document("\tcode line 1\n\tcode line 2\n");
match &elems[0] {
MarkdownElement::CodeBlock { language, lines } => {
assert!(language.is_none());
assert_eq!(lines, &["code line 1", "code line 2"]);
}
_ => panic!("expected code block"),
}
}
#[test]
fn parse_doc_indented_code_with_blank_line() {
let elems = parse_document(" code line 1\n\n code line 2\n");
match &elems[0] {
MarkdownElement::CodeBlock { language, lines } => {
assert!(language.is_none());
assert_eq!(lines, &["code line 1", "", "code line 2"]);
}
_ => panic!("expected code block"),
}
}
#[test]
fn parse_doc_indented_code_not_consumed_by_paragraph() {
let elems = parse_document("text\n code\n");
assert_eq!(elems.len(), 2);
assert!(matches!(elems[0], MarkdownElement::Paragraph { .. }));
assert!(matches!(elems[1], MarkdownElement::CodeBlock { .. }));
}
}