use super::*;
pub(super) struct MarkdownParser<'a> {
input: &'a str,
events: Vec<MarkdownEvent>,
index: usize,
preserve_raw: bool,
pub(super) fidelity: MarkupFidelity,
}
impl<'a> MarkdownParser<'a> {
pub(super) fn new(
input: &'a str,
events: Vec<MarkdownEvent>,
opts: &MarkupDecodeOptions,
) -> Self {
Self {
input,
events,
index: 0,
preserve_raw: opts.preserve_raw,
fidelity: MarkupFidelity::exact(markdown_id()),
}
}
pub(super) fn parse_blocks_until<F>(&mut self, stop: F) -> Vec<MarkupBlock>
where
F: Fn(&TagEnd) -> bool + Copy,
{
let mut blocks = Vec::new();
let mut loose = Vec::new();
while let Some((event, range)) = self.next() {
match event {
Event::End(end) if stop(&end) => {
self.index -= 1;
break;
}
Event::End(_) => self.flush_loose(&mut blocks, &mut loose),
Event::Start(tag) => {
self.flush_loose(&mut blocks, &mut loose);
self.push_block(tag, range, &mut blocks);
}
Event::DisplayMath(text) => {
self.flush_loose(&mut blocks, &mut loose);
blocks.push(MarkupBlock::MathBlock {
source: tex_math(text),
span: Some(span(range.start, range.end)),
});
}
Event::Rule => {
self.flush_loose(&mut blocks, &mut loose);
if let Some(raw) = self.raw_block(self.slice(&range), "rule", &range) {
blocks.push(raw);
}
}
other => self.push_inline_event(other, range, &mut loose),
}
}
self.flush_loose(&mut blocks, &mut loose);
blocks
}
fn push_block(
&mut self,
tag: Tag<'static>,
range: Range<usize>,
blocks: &mut Vec<MarkupBlock>,
) {
match tag {
Tag::Paragraph => blocks.push(self.parse_paragraph(range, Vec::new())),
Tag::Heading { level, id, .. } => blocks.push(self.parse_heading(level, id, range)),
Tag::CodeBlock(kind) => blocks.push(self.parse_code_block(kind, range)),
Tag::BlockQuote(_) => blocks.push(self.parse_quote(range)),
Tag::List(start) => blocks.push(self.parse_list(start.is_some(), range)),
Tag::Table(_) => blocks.push(self.parse_table(range)),
Tag::HtmlBlock => {
if let Some(raw) = self.raw_container(range, TagEnd::HtmlBlock, "html-block") {
blocks.push(raw);
}
}
Tag::FootnoteDefinition(_) => {
if let Some(raw) =
self.raw_container(range, TagEnd::FootnoteDefinition, "footnote-definition")
{
blocks.push(raw);
}
}
other => {
let end = other.to_end();
if let Some(raw) = self.raw_container(range, end, "unsupported-block") {
blocks.push(raw);
}
}
}
}
fn parse_paragraph(&mut self, start: Range<usize>, mut prefix: Vec<Inline>) -> MarkupBlock {
let (mut content, end) = self.collect_inlines_until(|end| *end == TagEnd::Paragraph);
prefix.append(&mut content);
if prefix.len() == 1
&& matches!(prefix[0], Inline::Math(_))
&& self
.input
.get(start.start..end.end)
.map(str::trim)
.is_some_and(|source| source.starts_with("$$") && source.ends_with("$$"))
{
let Inline::Math(source) = prefix.remove(0) else {
unreachable!();
};
return MarkupBlock::MathBlock {
source,
span: Some(span(start.start, end.end)),
};
}
MarkupBlock::Paragraph {
content: prefix,
span: Some(span(start.start, end.end)),
}
}
fn parse_heading(
&mut self,
level: HeadingLevel,
id: Option<pulldown_cmark::CowStr<'static>>,
start: Range<usize>,
) -> MarkupBlock {
let (text, end) = self.collect_inlines_until(|end| matches!(end, TagEnd::Heading(_)));
MarkupBlock::Heading {
level: heading_level(level),
text,
id: id.map(|value| value.to_string()),
span: Some(span(start.start, end.end)),
}
}
fn parse_code_block(
&mut self,
kind: CodeBlockKind<'static>,
start: Range<usize>,
) -> MarkupBlock {
let mut code = String::new();
let mut end = start.clone();
while let Some((event, range)) = self.next() {
end = range.clone();
match event {
Event::End(TagEnd::CodeBlock) => break,
Event::Text(text) => code.push_str(&text),
Event::Code(text) => code.push_str(&text),
Event::SoftBreak | Event::HardBreak => code.push('\n'),
_ => {}
}
}
let lang = match kind {
CodeBlockKind::Fenced(info) => info.split_whitespace().next().map(str::to_owned),
CodeBlockKind::Indented => None,
};
if matches!(lang.as_deref(), Some("math" | "tex")) {
MarkupBlock::MathBlock {
source: MathSource {
notation: "tex".to_owned(),
text: code,
},
span: Some(span(start.start, end.end)),
}
} else {
MarkupBlock::CodeBlock {
lang,
code,
span: Some(span(start.start, end.end)),
}
}
}
fn parse_quote(&mut self, start: Range<usize>) -> MarkupBlock {
let blocks = self.parse_blocks_until(|end| matches!(end, TagEnd::BlockQuote(_)));
let end = self.consume_end(|end| matches!(end, TagEnd::BlockQuote(_)), &start);
MarkupBlock::Quote {
blocks,
span: Some(span(start.start, end.end)),
}
}
fn parse_list(&mut self, ordered: bool, start: Range<usize>) -> MarkupBlock {
let mut items = Vec::new();
let mut end = start.clone();
while let Some((event, range)) = self.next() {
end = range.clone();
match event {
Event::End(TagEnd::List(_)) => break,
Event::Start(Tag::Item) => {
let item = self.parse_blocks_until(|end| *end == TagEnd::Item);
end = self.consume_end(|end| *end == TagEnd::Item, &range);
items.push(item);
}
_ => {}
}
}
MarkupBlock::List {
ordered,
items,
span: Some(span(start.start, end.end)),
}
}
fn parse_table(&mut self, start: Range<usize>) -> MarkupBlock {
let mut header = Vec::new();
let mut rows = Vec::new();
let mut current_row: Option<Vec<Vec<Inline>>> = None;
let mut in_head = false;
let mut end = start.clone();
while let Some((event, range)) = self.next() {
end = range.clone();
match event {
Event::End(TagEnd::Table) => break,
Event::Start(Tag::TableHead) => {
in_head = true;
current_row = Some(Vec::new());
}
Event::End(TagEnd::TableHead) => {
if let Some(row) = current_row.take() {
header = row;
}
in_head = false;
}
Event::Start(Tag::TableRow) => current_row = Some(Vec::new()),
Event::End(TagEnd::TableRow) => {
if let Some(row) = current_row.take() {
if in_head {
header = row;
} else {
rows.push(row);
}
}
}
Event::Start(Tag::TableCell) => {
let (cell, cell_end) =
self.collect_inlines_until(|end| *end == TagEnd::TableCell);
end = cell_end;
current_row.get_or_insert_with(Vec::new).push(cell);
}
_ => {}
}
}
MarkupBlock::Table {
header,
rows,
span: Some(span(start.start, end.end)),
}
}
fn collect_inlines_until<F>(&mut self, stop: F) -> (Vec<Inline>, Range<usize>)
where
F: Fn(&TagEnd) -> bool + Copy,
{
let mut items = Vec::new();
let mut end = self.current_end();
while let Some((event, range)) = self.next() {
end = range.clone();
match event {
Event::End(tag_end) if stop(&tag_end) => break,
other => self.push_inline_event(other, range, &mut items),
}
}
(items, end)
}
fn push_inline_event(
&mut self,
event: Event<'static>,
range: Range<usize>,
items: &mut Vec<Inline>,
) {
match event {
Event::Text(text) => items.push(Inline::Text(text.to_string())),
Event::Code(text) => items.push(Inline::Code(text.to_string())),
Event::InlineMath(text) | Event::DisplayMath(text) => {
items.push(Inline::Math(tex_math(text)));
}
Event::SoftBreak | Event::HardBreak => items.push(Inline::Text("\n".to_owned())),
Event::Html(text) | Event::InlineHtml(text) => {
if let Some(raw) = self.raw_inline(text.to_string(), "html") {
items.push(raw);
}
}
Event::FootnoteReference(label) => {
if let Some(raw) = self.raw_inline(format!("[^{label}]"), "footnote-reference") {
items.push(raw);
}
}
Event::TaskListMarker(checked) => {
let marker = if checked { "[x] " } else { "[ ] " };
if let Some(raw) = self.raw_inline(marker.to_owned(), "task-list-marker") {
items.push(raw);
}
}
Event::Rule => {
if let Some(raw) = self.raw_inline(self.slice(&range), "rule") {
items.push(raw);
}
}
Event::Start(Tag::Emphasis) => {
let (children, _) = self.collect_inlines_until(|end| *end == TagEnd::Emphasis);
items.push(Inline::Emph(children));
}
Event::Start(Tag::Strong) => {
let (children, _) = self.collect_inlines_until(|end| *end == TagEnd::Strong);
items.push(Inline::Strong(children));
}
Event::Start(Tag::Link { dest_url, .. }) => {
let (label, _) = self.collect_inlines_until(|end| *end == TagEnd::Link);
items.push(Inline::Link {
label,
target: dest_url.to_string(),
});
}
Event::Start(tag) => {
let end = tag.to_end();
if let Some(raw) = self.raw_inline_container(range, end, "unsupported-inline") {
items.push(raw);
}
}
Event::End(_) => {}
}
}
fn raw_container(
&mut self,
start: Range<usize>,
target: TagEnd,
path: &str,
) -> Option<MarkupBlock> {
let (raw, end) = self.consume_raw_container(start.clone(), target);
self.raw_block(raw, path, &(start.start..end))
}
fn raw_inline_container(
&mut self,
start: Range<usize>,
target: TagEnd,
path: &str,
) -> Option<Inline> {
let (raw, _) = self.consume_raw_container(start, target);
self.raw_inline(raw, path)
}
fn consume_raw_container(&mut self, start: Range<usize>, target: TagEnd) -> (String, usize) {
let mut depth = 1usize;
let mut end = start.end;
while let Some((event, range)) = self.next() {
end = range.end;
match event {
Event::Start(tag) if tag.to_end() == target => depth += 1,
Event::End(tag_end) if tag_end == target => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
}
(
self.input.get(start.start..end).unwrap_or("").to_owned(),
end,
)
}
fn raw_block(&mut self, raw: String, path: &str, range: &Range<usize>) -> Option<MarkupBlock> {
if self.preserve_raw {
self.fidelity.preserved_raw.push(raw.clone());
Some(MarkupBlock::Raw {
backend: markdown_id(),
text: raw,
span: Some(span(range.start, range.end)),
})
} else {
self.drop_raw(path, "unsupported markdown block");
None
}
}
fn raw_inline(&mut self, raw: String, path: &str) -> Option<Inline> {
if self.preserve_raw {
self.fidelity.preserved_raw.push(raw.clone());
Some(Inline::Raw {
backend: markdown_id(),
text: raw,
})
} else {
self.drop_raw(path, "unsupported markdown inline");
None
}
}
fn drop_raw(&mut self, path: &str, reason: &str) {
self.fidelity.dropped.push(MarkupLoss {
path: path.to_owned(),
reason: reason.to_owned(),
});
}
fn flush_loose(&mut self, blocks: &mut Vec<MarkupBlock>, loose: &mut Vec<Inline>) {
if !loose.is_empty() {
blocks.push(MarkupBlock::Paragraph {
content: mem::take(loose),
span: None,
});
}
}
fn consume_end<F>(&mut self, stop: F, fallback: &Range<usize>) -> Range<usize>
where
F: Fn(&TagEnd) -> bool,
{
match self.next() {
Some((Event::End(end), range)) if stop(&end) => range,
Some(_) => fallback.clone(),
None => fallback.clone(),
}
}
fn next(&mut self) -> Option<MarkdownEvent> {
let event = self.events.get(self.index).cloned();
if event.is_some() {
self.index += 1;
}
event
}
fn current_end(&self) -> Range<usize> {
self.events
.get(self.index.saturating_sub(1))
.map(|(_, range)| range.clone())
.unwrap_or(0..0)
}
fn slice(&self, range: &Range<usize>) -> String {
self.input.get(range.clone()).unwrap_or("").to_owned()
}
}