use super::ast::Block;
use super::inline::parse_inlines;
use super::{list, table};
pub(crate) fn parse_markdown(input: &str) -> Vec<Block> {
let lines: Vec<String> = input.lines().map(str::to_string).collect();
parse_blocks(&lines)
}
pub(super) fn parse_blocks(lines: &[String]) -> Vec<Block> {
let mut blocks = Vec::new();
let mut i = 0;
while i < lines.len() {
let t = lines[i].trim();
if t.is_empty() {
i += 1;
continue;
}
if is_fence(t) {
let lang = fence_lang(t);
i += 1;
let mut buf = Vec::new();
while i < lines.len() && !is_fence(lines[i].trim()) {
buf.push(lines[i].clone());
i += 1;
}
i += 1; blocks.push(Block::Code {
lang,
text: buf.join("\n"),
});
continue;
}
if let Some((tbl, next)) = table::try_parse(lines, i) {
blocks.push(Block::Table(tbl));
i = next;
continue;
}
if let Some((level, content)) = heading(t) {
blocks.push(Block::Heading {
level,
content: parse_inlines(content),
});
i += 1;
continue;
}
if is_divider(t) {
blocks.push(Block::Divider);
i += 1;
continue;
}
if t == "$$" {
i += 1;
let mut buf = Vec::new();
while i < lines.len() && lines[i].trim() != "$$" {
buf.push(lines[i].clone());
i += 1;
}
i += 1; blocks.push(Block::Math(buf.join("\n")));
continue;
}
if let Some(inner) = t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$"))
&& !inner.is_empty()
{
blocks.push(Block::Math(inner.trim().to_string()));
i += 1;
continue;
}
if t.starts_with('>') {
let mut buf = Vec::new();
while i < lines.len() && lines[i].trim_start().starts_with('>') {
let l = lines[i].trim_start();
let stripped = l.strip_prefix('>').unwrap_or(l);
buf.push(stripped.strip_prefix(' ').unwrap_or(stripped).to_string());
i += 1;
}
blocks.push(Block::Quote(parse_blocks(&buf)));
continue;
}
if list::is_item(&lines[i]) {
blocks.push(Block::List(list::parse_list(lines, &mut i)));
continue;
}
let mut buf = Vec::new();
while i < lines.len() {
if lines[i].trim().is_empty() || starts_block(lines, i) {
break;
}
buf.push(lines[i].trim().to_string());
i += 1;
}
blocks.push(Block::Paragraph(parse_inlines(&buf.join(" "))));
}
blocks
}
fn starts_block(lines: &[String], i: usize) -> bool {
let t = lines[i].trim();
t.is_empty()
|| is_fence(t)
|| heading(t).is_some()
|| is_divider(t)
|| t.starts_with('>')
|| t == "$$"
|| list::is_item(&lines[i])
|| table::try_parse(lines, i).is_some()
}
fn is_fence(t: &str) -> bool {
t.starts_with("```")
}
fn fence_lang(t: &str) -> Option<String> {
let lang = t.trim_start_matches('`').trim();
(!lang.is_empty()).then(|| lang.to_string())
}
fn heading(t: &str) -> Option<(u8, &str)> {
let hashes = t.chars().take_while(|&c| c == '#').count();
if (1..=6).contains(&hashes) {
let rest = &t[hashes..];
if rest.is_empty() || rest.starts_with(' ') {
return Some((hashes as u8, rest.trim()));
}
}
None
}
fn is_divider(t: &str) -> bool {
let s: String = t.chars().filter(|c| !c.is_whitespace()).collect();
s.len() >= 3
&& (s.bytes().all(|b| b == b'-')
|| s.bytes().all(|b| b == b'*')
|| s.bytes().all(|b| b == b'_'))
}