use crate::event::*;
use crate::inline;
use crate::linkref;
use crate::parser::Parser;
use std::collections::HashMap;
const TAB: usize = 4;
#[derive(Default)]
pub struct StreamParser {
buf: Vec<u8>,
started: bool,
flushed: bool,
containers: Vec<Container>,
leaf: Leaf,
last_blank: bool,
refs: HashMap<String, LinkDef>,
gfm: bool,
gate: Vec<Slot>,
}
enum Slot {
Event(Event),
Deferred(Deferred),
}
struct Deferred {
text: String,
style: InlineStyle,
labels: Vec<String>,
}
enum ParaRun {
Resolved(Vec<Event>),
Deferred(Deferred),
}
enum Container {
BlockQuote,
List(ListFrame),
Item { indent: usize },
}
struct ListFrame {
ordered: bool,
marker: char,
start: u64,
loose: bool,
events: Vec<BufEvent>,
pending_blank: bool,
}
enum BufEvent {
Raw(Event),
ItemStart,
ItemEnd,
Para(Vec<Event>),
DeferPara {
prefix: Vec<Event>,
deferred: Deferred,
},
DeferRaw(Deferred),
}
#[derive(Default)]
enum Leaf {
#[default]
None,
Paragraph(String),
Indented(Vec<String>),
Fenced {
ch: u8,
len: usize,
indent: usize,
},
Table {
aligns: Vec<Alignment>,
},
Html {
end: HtmlEnd,
},
}
#[derive(Clone, Copy)]
enum HtmlEnd {
Marker(&'static str),
Blank,
}
struct Marker {
ordered: bool,
marker: char,
start: u64,
after: usize,
}
impl StreamParser {
pub fn new() -> Self {
Self::default()
}
pub fn new_gfm() -> Self {
StreamParser {
gfm: true,
..Self::default()
}
}
}
impl Parser for StreamParser {
fn write(&mut self, chunk: &[u8]) -> Vec<Event> {
let mut out = Vec::new();
self.buf.extend_from_slice(chunk);
while let Some(nl) = self.buf.iter().position(|&b| b == b'\n') {
let mut line: Vec<u8> = self.buf.drain(..=nl).collect();
line.pop(); if line.last() == Some(&b'\r') {
line.pop();
}
let s = String::from_utf8_lossy(&line).into_owned();
self.process_line(&s, &mut out);
}
out
}
fn flush(&mut self) -> Vec<Event> {
let mut out = Vec::new();
if self.flushed {
return out;
}
if !self.buf.is_empty() {
let line = std::mem::take(&mut self.buf);
let s = String::from_utf8_lossy(&line).into_owned();
self.process_line(&s, &mut out);
}
self.close_leaf(&mut out);
self.close_containers_to(0, &mut out);
if self.started {
self.gate
.push(Slot::Event(Event::exit(BlockKind::Document)));
}
self.flush_gate(&mut out);
self.flushed = true;
out
}
fn reset(&mut self) {
*self = Self::default();
}
}
impl StreamParser {
fn ensure_doc(&mut self, out: &mut Vec<Event>) {
if !self.started {
self.emit(out, Event::enter(BlockKind::Document));
self.started = true;
}
}
fn process_line(&mut self, raw: &str, out: &mut Vec<Event>) {
let mut cur = Cursor::new(raw);
let mut matched = 0usize; for c in &self.containers {
match c {
Container::BlockQuote => {
let save = cur.clone();
if cur.indent() <= 3 && cur.peek_nonspace() == Some(b'>') {
cur.advance_to_nonspace();
cur.bump(); if cur.peek() == Some(b' ') {
cur.bump();
} else if cur.peek() == Some(b'\t') {
cur.consume_tab_as_space();
}
matched += 1;
} else {
cur = save;
break;
}
}
Container::List(_) => {
matched += 1;
}
Container::Item { indent } => {
if cur.is_blank() {
matched += 1;
break;
}
if cur.indent() >= *indent {
cur.consume_cols(*indent);
matched += 1;
} else {
break;
}
}
}
}
let all_matched = matched == self.containers.len();
let blank = cur.is_blank();
self.dispatch(raw, cur, matched, all_matched, blank, out);
self.last_blank = blank;
}
fn dispatch(
&mut self,
raw: &str,
mut cur: Cursor,
matched: usize,
all_matched: bool,
blank: bool,
out: &mut Vec<Event>,
) {
if let Leaf::Fenced { ch, len, indent } = self.leaf {
if all_matched {
let t = cur.rest_str();
let tt = t.trim_start();
if is_closing_fence(tt, ch, len) {
self.close_leaf(out);
} else {
let stripped = strip_cols(&t, indent);
self.emit(out, Event::text(format!("{stripped}\n")));
}
return;
}
self.close_leaf(out);
self.close_containers_to(matched, out);
}
if let Leaf::Html { end } = self.leaf {
if all_matched {
let content = cur.rest_str();
match end {
HtmlEnd::Marker(marker) => {
self.emit(out, Event::text(format!("{content}\n")));
if contains_ci(&content, marker) {
self.close_leaf(out);
}
return;
}
HtmlEnd::Blank => {
if content.trim().is_empty() {
self.close_leaf(out);
} else {
self.emit(out, Event::text(format!("{content}\n")));
return;
}
}
}
if !matches!(self.leaf, Leaf::None) {
return;
}
} else {
self.close_leaf(out);
self.close_containers_to(matched, out);
}
}
if let Leaf::Table { aligns } = &self.leaf {
if all_matched && !blank {
let content = cur.rest_str();
if content.contains('|') {
let aligns = aligns.clone();
self.emit_row(split_row(&content), &aligns, out);
return;
}
}
self.close_leaf(out);
if !all_matched {
self.close_containers_to(matched, out);
}
}
if let Leaf::Indented(_) = &self.leaf {
if all_matched && (blank || cur.indent() >= TAB) {
if blank {
if let Leaf::Indented(lines) = &mut self.leaf {
lines.push(String::new());
}
return;
}
cur.consume_cols(TAB);
let line = cur.rest_str_with_partial_tab();
if let Leaf::Indented(lines) = &mut self.leaf {
lines.push(line);
}
return;
}
self.close_leaf(out);
if !all_matched {
self.close_containers_to(matched, out);
}
}
if blank {
if matches!(self.leaf, Leaf::Paragraph(_)) {
self.close_leaf(out);
}
self.note_blank_in_item();
return;
}
let lazy = !all_matched
&& matches!(self.leaf, Leaf::Paragraph(_))
&& self.can_lazily_continue(&cur);
if !lazy && !all_matched {
self.close_leaf(out);
self.close_containers_to(matched, out);
}
if !lazy {
self.open_new_containers(&mut cur, out);
}
if !lazy && matches!(self.containers.last(), Some(Container::List(_))) {
let keep = self.containers.len() - 1;
self.close_containers_to(keep, out);
}
self.parse_leaf(&cur, raw, lazy, out);
}
fn open_new_containers(&mut self, cur: &mut Cursor, out: &mut Vec<Event>) {
loop {
let indent = cur.indent();
if indent >= TAB {
break;
}
if indent <= 3 && cur.peek_nonspace() == Some(b'>') {
self.ensure_doc(out);
self.close_leaf(out);
if matches!(self.containers.last(), Some(Container::Item { .. })) {
self.commit_pending_blank();
}
self.emit(out, Event::enter(BlockKind::BlockQuote));
self.containers.push(Container::BlockQuote);
cur.advance_to_nonspace();
cur.bump();
if cur.peek() == Some(b' ') {
cur.bump();
} else if cur.peek() == Some(b'\t') {
cur.consume_tab_as_space();
}
continue;
}
if indent <= 3 && !is_thematic_break(&cur.rest_after_indent()) {
if let Some(m) = self.parse_marker(cur) {
self.start_list_item(cur, m, out);
continue;
}
}
break;
}
}
fn parse_marker(&self, cur: &Cursor) -> Option<Marker> {
let rest = cur.rest_after_indent();
let m = list_marker(&rest)?;
if matches!(self.leaf, Leaf::Paragraph(_)) && !self.in_any_list() {
let empty = rest[m.after..].trim().is_empty();
if empty || (m.ordered && m.start != 1) {
return None;
}
}
Some(m)
}
fn start_list_item(&mut self, cur: &mut Cursor, m: Marker, out: &mut Vec<Event>) {
self.ensure_doc(out);
self.close_leaf(out);
let base_col = cur.col();
cur.advance_to_nonspace();
cur.consume_bytes(m.after);
let marker_width = cur.col() - base_col;
let spaces = cur.count_spaces();
let content_indent;
if cur.is_blank_from_here() {
content_indent = marker_width + 1;
} else if (1..=TAB).contains(&spaces) {
content_indent = marker_width + spaces;
cur.consume_cols_max(spaces);
} else {
content_indent = marker_width + 1;
cur.consume_cols_max(1);
}
let same_list = matches!(self.containers.last(), Some(Container::List(l))
if l.ordered == m.ordered && l.marker == m.marker);
let diff_list = matches!(self.containers.last(), Some(Container::List(_))) && !same_list;
if diff_list {
let keep = self.containers.len() - 1;
self.close_containers_to(keep, out);
}
if same_list {
self.commit_pending_blank();
} else {
if matches!(self.containers.last(), Some(Container::Item { .. })) {
self.commit_pending_blank();
}
let frame = ListFrame {
ordered: m.ordered,
marker: m.marker,
start: m.start,
loose: false,
events: Vec::new(),
pending_blank: false,
};
self.containers.push(Container::List(frame));
}
self.emit(out, Event::enter(BlockKind::ListItem));
self.mark_item_start();
self.containers.push(Container::Item {
indent: content_indent,
});
}
fn parse_leaf(&mut self, cur: &Cursor, raw: &str, lazy: bool, out: &mut Vec<Event>) {
let _ = raw;
let content = cur.rest_str();
let trimmed = content.trim_start();
let indent = cur.indent();
if lazy {
if let Leaf::Paragraph(p) = &mut self.leaf {
p.push('\n');
p.push_str(&content);
}
return;
}
if matches!(self.leaf, Leaf::None)
&& matches!(self.containers.last(), Some(Container::Item { .. }))
{
self.commit_pending_blank();
}
if indent >= TAB {
if let Leaf::Paragraph(p) = &mut self.leaf {
p.push('\n');
p.push_str(&content);
return;
}
self.ensure_doc(out);
let mut c = cur.clone();
c.consume_cols(TAB);
let line = c.rest_str_with_partial_tab();
self.leaf = Leaf::Indented(vec![line]);
return;
}
let in_paragraph = matches!(self.leaf, Leaf::Paragraph(_));
if let Some(end) = html_block_start(trimmed, in_paragraph) {
self.ensure_doc(out);
self.close_leaf(out);
let html_raw_text = matches!(
end,
HtmlEnd::Marker("</script>" | "</style>" | "</pre>" | "</textarea>")
);
self.emit(
out,
Event::EnterBlock {
block: BlockKind::HtmlBlock,
data: BlockData {
html_raw_text,
..Default::default()
},
span: Span::default(),
},
);
self.emit(out, Event::text(format!("{trimmed}\n")));
match end {
HtmlEnd::Marker(marker) if contains_ci(trimmed, marker) => {
self.close_leaf(out);
}
_ => self.leaf = Leaf::Html { end },
}
return;
}
if let Some((ch, len, info)) = fence_start(trimmed) {
self.ensure_doc(out);
self.close_leaf(out);
let data = BlockData {
info,
..Default::default()
};
self.emit(
out,
Event::EnterBlock {
block: BlockKind::FencedCode,
data,
span: Span::default(),
},
);
self.leaf = Leaf::Fenced { ch, len, indent };
return;
}
if let Some((level, htext)) = atx_heading(trimmed) {
self.ensure_doc(out);
self.close_leaf(out);
let data = BlockData {
level,
..Default::default()
};
self.emit(
out,
Event::EnterBlock {
block: BlockKind::Heading,
data,
span: Span::default(),
},
);
self.parse_inline(htext, out);
self.emit(out, Event::exit(BlockKind::Heading));
return;
}
if let Leaf::Paragraph(_) = &self.leaf {
if let Some(level) = setext_underline(trimmed) {
if let Leaf::Paragraph(text) = std::mem::take(&mut self.leaf) {
let body = self.consume_refdefs(&text);
if body.is_empty() {
self.leaf = Leaf::None;
} else {
let data = BlockData {
level,
..Default::default()
};
self.emit(
out,
Event::EnterBlock {
block: BlockKind::Heading,
data,
span: Span::default(),
},
);
self.parse_inline(body.trim_end(), out);
self.emit(out, Event::exit(BlockKind::Heading));
return;
}
}
}
}
if is_thematic_break(trimmed) {
self.ensure_doc(out);
self.close_leaf(out);
self.emit(out, Event::enter(BlockKind::ThematicBreak));
self.emit(out, Event::exit(BlockKind::ThematicBreak));
return;
}
match &mut self.leaf {
Leaf::Paragraph(p) => {
if !p.contains('\n') && p.contains('|') {
if let Some(aligns) = parse_delim_row(trimmed) {
let headers = split_row(p);
if headers.len() == aligns.len() {
let header = std::mem::take(p);
self.leaf = Leaf::None;
self.start_table(&header, aligns, out);
return;
}
}
}
p.push('\n');
p.push_str(&content);
}
_ => {
if trimmed.is_empty() {
return;
}
self.ensure_doc(out);
self.leaf = Leaf::Paragraph(trimmed.to_string());
}
}
}
fn can_lazily_continue(&self, cur: &Cursor) -> bool {
let rest = cur.rest_after_indent();
let trimmed = rest.trim_start();
if trimmed.is_empty() {
return false;
}
if cur.indent() >= TAB {
return true;
}
if is_thematic_break(trimmed) {
return false;
}
if atx_heading(trimmed).is_some() {
return false;
}
if fence_start(trimmed).is_some() {
return false;
}
if cur.indent() <= 3 && cur.peek_nonspace() == Some(b'>') {
return false;
}
if html_block_start(trimmed, true).is_some() {
return false;
}
if let Some(m) = list_marker(trimmed) {
if self.in_any_list() {
return false;
}
let empty = trimmed[m.after..].trim().is_empty();
if !(empty || (m.ordered && m.start != 1)) {
return false;
}
}
true
}
fn emit(&mut self, out: &mut Vec<Event>, ev: Event) {
if let Some(frame) = self.innermost_list_mut() {
frame.events.push(BufEvent::Raw(ev));
} else {
self.gate.push(Slot::Event(ev));
self.drain_gate(out);
}
}
fn emit_para(&mut self, out: &mut Vec<Event>, para: Vec<Event>) {
if let Some(frame) = self.innermost_list_mut() {
frame.events.push(BufEvent::Para(para));
} else {
self.gate
.push(Slot::Event(Event::enter(BlockKind::Paragraph)));
for ev in para {
self.gate.push(Slot::Event(ev));
}
self.gate
.push(Slot::Event(Event::exit(BlockKind::Paragraph)));
self.drain_gate(out);
}
}
fn emit_buf_para_defer(&mut self, prefix: Vec<Event>, deferred: Deferred) {
if let Some(frame) = self.innermost_list_mut() {
frame.events.push(BufEvent::DeferPara { prefix, deferred });
}
}
fn emit_buf_raw_defer(&mut self, deferred: Deferred) {
if let Some(frame) = self.innermost_list_mut() {
frame.events.push(BufEvent::DeferRaw(deferred));
}
}
fn emit_defer_para(&mut self, out: &mut Vec<Event>, prefix: Vec<Event>, deferred: Deferred) {
self.gate
.push(Slot::Event(Event::enter(BlockKind::Paragraph)));
for ev in prefix {
self.gate.push(Slot::Event(ev));
}
self.gate.push(Slot::Deferred(deferred));
self.gate
.push(Slot::Event(Event::exit(BlockKind::Paragraph)));
self.drain_gate(out);
}
fn drain_gate(&mut self, out: &mut Vec<Event>) {
let mut release = 0;
for slot in &self.gate {
match slot {
Slot::Event(_) => release += 1,
Slot::Deferred(d) => {
if d.labels.iter().all(|l| self.refs.contains_key(l)) {
release += 1;
} else {
break;
}
}
}
}
let released: Vec<Slot> = self.gate.drain(..release).collect();
self.emit_slots(released, out);
}
fn flush_gate(&mut self, out: &mut Vec<Event>) {
let slots: Vec<Slot> = std::mem::take(&mut self.gate);
self.emit_slots(slots, out);
}
fn emit_slots(&self, slots: Vec<Slot>, out: &mut Vec<Event>) {
for slot in slots {
match slot {
Slot::Event(ev) => out.push(ev),
Slot::Deferred(d) => {
inline::parse(&d.text, &d.style, &self.refs, self.gfm, out);
}
}
}
}
fn mark_item_start(&mut self) {
if let Some(frame) = self.innermost_list_mut() {
frame.events.push(BufEvent::ItemStart);
}
}
fn mark_item_end(&mut self) {
if let Some(frame) = self.innermost_list_mut() {
frame.events.push(BufEvent::ItemEnd);
}
}
fn item_is_empty(&mut self) -> bool {
matches!(
self.innermost_list_mut().and_then(|f| f.events.last()),
Some(BufEvent::ItemStart)
)
}
fn note_blank_in_item(&mut self) {
for c in self.containers.iter_mut().rev() {
match c {
Container::List(l) => l.pending_blank = true,
Container::BlockQuote => break,
Container::Item { .. } => {}
}
}
}
fn commit_pending_blank(&mut self) {
let mut committed = false;
for c in self.containers.iter_mut().rev() {
if let Container::List(l) = c {
if !committed {
if l.pending_blank {
l.loose = true;
}
committed = true;
}
l.pending_blank = false;
}
}
}
fn innermost_list_mut(&mut self) -> Option<&mut ListFrame> {
for c in self.containers.iter_mut().rev() {
if let Container::List(l) = c {
return Some(l);
}
}
None
}
fn in_any_list(&self) -> bool {
self.containers
.iter()
.any(|c| matches!(c, Container::List(_)))
}
fn para_is_direct_list_child(&self) -> bool {
matches!(self.containers.last(), Some(Container::Item { .. }))
}
fn close_containers_to(&mut self, keep: usize, out: &mut Vec<Event>) {
while self.containers.len() > keep {
self.close_leaf(out);
match self.containers.pop().unwrap() {
Container::BlockQuote => {
self.emit(out, Event::exit(BlockKind::BlockQuote));
}
Container::Item { .. } => {
self.mark_item_end();
self.emit(out, Event::exit(BlockKind::ListItem));
}
Container::List(frame) => {
self.flush_list(frame, out);
}
}
}
}
fn flush_list(&mut self, frame: ListFrame, out: &mut Vec<Event>) {
let tight = !frame.loose;
let data = BlockData {
list: Some(ListData {
ordered: frame.ordered,
start: frame.start,
tight,
marker: frame.marker,
}),
..Default::default()
};
let mut body: Vec<Slot> = Vec::new();
let mut item: Vec<Slot> = Vec::new();
let mut in_item = false;
let push_para = |target: &mut Vec<Slot>, prefix: Vec<Event>, run: ParaRun| {
if !tight {
target.push(Slot::Event(Event::enter(BlockKind::Paragraph)));
}
for ev in prefix {
target.push(Slot::Event(ev));
}
match run {
ParaRun::Resolved(inner) => target.extend(inner.into_iter().map(Slot::Event)),
ParaRun::Deferred(d) => target.push(Slot::Deferred(d)),
}
if !tight {
target.push(Slot::Event(Event::exit(BlockKind::Paragraph)));
}
};
for be in frame.events {
match be {
BufEvent::ItemStart => {
in_item = true;
item.clear();
}
BufEvent::ItemEnd => {
let wrapped = wrap_item_children(std::mem::take(&mut item));
body.extend(wrapped);
in_item = false;
}
BufEvent::Para(inner) => {
let target = if in_item { &mut item } else { &mut body };
push_para(target, Vec::new(), ParaRun::Resolved(inner));
}
BufEvent::DeferPara { prefix, deferred } => {
let target = if in_item { &mut item } else { &mut body };
push_para(target, prefix, ParaRun::Deferred(deferred));
}
BufEvent::DeferRaw(deferred) => {
let target = if in_item { &mut item } else { &mut body };
target.push(Slot::Deferred(deferred));
}
BufEvent::Raw(ev) => {
let target = if in_item { &mut item } else { &mut body };
target.push(Slot::Event(ev));
}
}
}
if let Some(parent) = self.innermost_list_mut() {
parent.events.push(BufEvent::Raw(Event::EnterBlock {
block: BlockKind::List,
data,
span: Span::default(),
}));
for slot in body {
match slot {
Slot::Event(ev) => parent.events.push(BufEvent::Raw(ev)),
Slot::Deferred(deferred) => parent.events.push(BufEvent::DeferRaw(deferred)),
}
}
parent
.events
.push(BufEvent::Raw(Event::exit(BlockKind::List)));
} else {
self.gate.push(Slot::Event(Event::EnterBlock {
block: BlockKind::List,
data,
span: Span::default(),
}));
self.gate.extend(body);
self.gate.push(Slot::Event(Event::exit(BlockKind::List)));
self.drain_gate(out);
}
}
fn close_leaf(&mut self, out: &mut Vec<Event>) {
match std::mem::take(&mut self.leaf) {
Leaf::None => {}
Leaf::Paragraph(text) => {
let body = self.consume_refdefs(&text);
let body = body.trim_end();
if body.is_empty() {
return;
}
let mut prefix = Vec::new();
let mut body = body;
if self.gfm && self.para_is_direct_list_child() && self.item_is_empty() {
if let Some((checked, rest)) = task_marker(body) {
prefix.push(Event::Text {
text: format!(
"<input {}disabled=\"\" type=\"checkbox\"> ",
if checked { "checked=\"\" " } else { "" }
),
style: InlineStyle {
raw_html: true,
..Default::default()
},
span: Span::default(),
});
body = rest;
}
}
let style = InlineStyle::default();
let mut inner = Vec::new();
let mut labels = Vec::new();
inline::parse_collect_unresolved(
body,
&style,
&self.refs,
self.gfm,
&mut inner,
&mut labels,
);
let deferred = (!labels.is_empty()).then(|| Deferred {
text: body.to_string(),
style,
labels,
});
if self.para_is_direct_list_child() {
match deferred {
Some(deferred) => self.emit_buf_para_defer(prefix, deferred),
None => {
let mut run = prefix;
run.extend(inner);
self.emit_para(out, run);
}
}
} else if self.in_any_list() {
self.emit(out, Event::enter(BlockKind::Paragraph));
for ev in prefix {
self.emit(out, ev);
}
match deferred {
Some(deferred) => self.emit_buf_raw_defer(deferred),
None => {
for ev in inner {
self.emit(out, ev);
}
}
}
self.emit(out, Event::exit(BlockKind::Paragraph));
} else {
match deferred {
Some(deferred) => self.emit_defer_para(out, prefix, deferred),
None => {
let mut run = prefix;
run.extend(inner);
self.emit_para(out, run);
}
}
}
}
Leaf::Indented(mut lines) => {
while lines.last().map(|l| l.trim().is_empty()) == Some(true) {
lines.pop();
}
if lines.is_empty() {
return;
}
self.emit(out, Event::enter(BlockKind::IndentedCode));
let mut text = lines.join("\n");
text.push('\n');
self.emit(out, Event::text(text));
self.emit(out, Event::exit(BlockKind::IndentedCode));
}
Leaf::Fenced { .. } => {
self.emit(out, Event::exit(BlockKind::FencedCode));
}
Leaf::Table { .. } => {
self.emit(out, Event::exit(BlockKind::Table));
}
Leaf::Html { .. } => {
self.emit(out, Event::exit(BlockKind::HtmlBlock));
}
}
}
fn parse_inline(&mut self, text: &str, out: &mut Vec<Event>) {
let style = InlineStyle::default();
let mut inner = Vec::new();
let mut labels = Vec::new();
inline::parse_collect_unresolved(
text,
&style,
&self.refs,
self.gfm,
&mut inner,
&mut labels,
);
let deferred = (!labels.is_empty()).then(|| Deferred {
text: text.to_string(),
style,
labels,
});
if self.in_any_list() {
match deferred {
Some(deferred) => self.emit_buf_raw_defer(deferred),
None => {
for ev in inner {
self.emit(out, ev);
}
}
}
} else {
match deferred {
Some(deferred) => {
self.gate.push(Slot::Deferred(deferred));
self.drain_gate(out);
}
None => {
for ev in inner {
self.emit(out, ev);
}
}
}
}
}
fn consume_refdefs(&mut self, text: &str) -> String {
let b = text.as_bytes();
let mut pos = 0;
loop {
let line_start = pos;
let mut p = pos;
let mut spaces = 0;
while p < b.len() && b[p] == b' ' {
spaces += 1;
p += 1;
}
if spaces > 3 {
break;
}
match parse_refdef(b, p) {
Some((label, def, next)) => {
if let Some(norm) = linkref::normalize_label(&label) {
self.refs.entry(norm).or_insert(def);
pos = next;
} else {
break;
}
}
None => {
pos = line_start;
break;
}
}
}
text[pos..].to_string()
}
fn start_table(&mut self, header: &str, aligns: Vec<Alignment>, out: &mut Vec<Event>) {
let data = BlockData {
alignment: aligns.clone(),
..Default::default()
};
self.emit(
out,
Event::EnterBlock {
block: BlockKind::Table,
data,
span: Span::default(),
},
);
self.emit_row(split_row(header), &aligns, out);
self.leaf = Leaf::Table { aligns };
}
fn emit_row(&mut self, mut cells: Vec<String>, aligns: &[Alignment], out: &mut Vec<Event>) {
cells.resize(aligns.len(), String::new());
self.emit(out, Event::enter(BlockKind::TableRow));
for cell in cells {
self.emit(out, Event::enter(BlockKind::TableCell));
self.parse_inline(cell.trim(), out);
self.emit(out, Event::exit(BlockKind::TableCell));
}
self.emit(out, Event::exit(BlockKind::TableRow));
}
}
#[derive(Clone)]
struct Cursor {
bytes: Vec<u8>,
pos: usize,
column: usize,
partial_tab: usize,
}
impl Cursor {
fn new(line: &str) -> Self {
Cursor {
bytes: line.as_bytes().to_vec(),
pos: 0,
column: 0,
partial_tab: 0,
}
}
fn col(&self) -> usize {
self.column
}
fn peek(&self) -> Option<u8> {
self.bytes.get(self.pos).copied()
}
fn peek_nonspace(&self) -> Option<u8> {
let mut i = self.pos;
while i < self.bytes.len() && matches!(self.bytes[i], b' ' | b'\t') {
i += 1;
}
self.bytes.get(i).copied()
}
fn indent(&self) -> usize {
let mut col = self.column;
let mut i = self.pos;
let start = col;
while i < self.bytes.len() {
match self.bytes[i] {
b' ' => {
col += 1;
i += 1;
}
b'\t' => {
col += TAB - (col % TAB);
i += 1;
}
_ => break,
}
}
col - start
}
fn is_blank(&self) -> bool {
self.bytes[self.pos..]
.iter()
.all(|&b| matches!(b, b' ' | b'\t'))
}
fn is_blank_from_here(&self) -> bool {
self.is_blank()
}
fn bump(&mut self) {
if let Some(b) = self.peek() {
match b {
b'\t' => {
self.column += TAB - (self.column % TAB);
self.partial_tab = 0;
}
_ => self.column += 1,
}
self.pos += 1;
}
}
fn consume_bytes(&mut self, n: usize) {
for _ in 0..n {
self.bump();
}
}
fn advance_to_nonspace(&mut self) {
while matches!(self.peek(), Some(b' ') | Some(b'\t')) {
self.bump();
}
}
fn consume_cols(&mut self, cols: usize) {
let target = self.column + cols;
while self.column < target {
match self.peek() {
Some(b' ') => self.bump(),
Some(b'\t') => {
let width = TAB - (self.column % TAB);
if self.column + width <= target {
self.bump();
} else {
let take = target - self.column;
self.partial_tab += take;
self.column = target;
}
}
_ => break,
}
}
}
fn consume_cols_max(&mut self, cols: usize) {
self.consume_cols(cols);
}
fn count_spaces(&self) -> usize {
self.indent()
}
fn rest_str(&self) -> String {
String::from_utf8_lossy(&self.bytes[self.pos..]).into_owned()
}
fn rest_after_indent(&self) -> String {
let mut i = self.pos;
while i < self.bytes.len() && matches!(self.bytes[i], b' ' | b'\t') {
i += 1;
}
String::from_utf8_lossy(&self.bytes[i..]).into_owned()
}
fn rest_str_with_partial_tab(&self) -> String {
if self.partial_tab > 0 && self.peek() == Some(b'\t') {
let remaining = TAB - (self.column % TAB);
let mut s = " ".repeat(remaining);
s.push_str(&String::from_utf8_lossy(&self.bytes[self.pos + 1..]));
return s;
}
self.rest_str()
}
fn consume_tab_as_space(&mut self) {
if self.peek() == Some(b'\t') {
let width = TAB - (self.column % TAB);
if width <= 1 {
self.bump();
} else {
self.partial_tab += 1;
self.column += 1;
}
}
}
}
fn strip_cols(s: &str, cols: usize) -> String {
let b = s.as_bytes();
let mut col = 0;
let mut i = 0;
while i < b.len() && col < cols {
match b[i] {
b' ' => {
col += 1;
i += 1;
}
b'\t' => {
let width = TAB - (col % TAB);
if col + width <= cols {
col += width;
i += 1;
} else {
let leftover = (col + width) - cols;
let mut out = " ".repeat(leftover);
out.push_str(&String::from_utf8_lossy(&b[i + 1..]));
return out;
}
}
_ => break,
}
}
String::from_utf8_lossy(&b[i..]).into_owned()
}
fn is_block_enter(ev: &Event) -> bool {
matches!(
ev,
Event::EnterBlock {
block: BlockKind::List
| BlockKind::BlockQuote
| BlockKind::FencedCode
| BlockKind::IndentedCode
| BlockKind::Heading
| BlockKind::ThematicBreak
| BlockKind::HtmlBlock
| BlockKind::Table
| BlockKind::Paragraph,
..
}
)
}
fn wrap_item_children(slots: Vec<Slot>) -> Vec<Slot> {
let mut out = Vec::with_capacity(slots.len() + 2);
let mut depth = 0i32;
let mut prev_block = false;
for slot in slots {
match &slot {
Slot::Event(Event::EnterBlock { .. }) => {
if depth == 0 {
if matches!(&slot, Slot::Event(ev) if is_block_enter(ev)) {
if !prev_block {
out.push(Slot::Event(Event::text("\n")));
}
prev_block = true;
} else {
prev_block = false;
}
}
depth += 1;
out.push(slot);
}
Slot::Event(Event::ExitBlock { .. }) => {
depth -= 1;
out.push(slot);
}
_ => {
if depth == 0 {
prev_block = false;
}
out.push(slot);
}
}
}
out
}
fn task_marker(body: &str) -> Option<(bool, &str)> {
let b = body.as_bytes();
if b.first() != Some(&b'[') || b.get(2) != Some(&b']') {
return None;
}
let checked = match b.get(1) {
Some(b' ') => false,
Some(b'x') | Some(b'X') => true,
_ => return None,
};
match b.get(3) {
Some(b' ') | Some(b'\t') => Some((checked, &body[4..])),
None => Some((checked, "")),
_ => None,
}
}
fn atx_heading(line: &str) -> Option<(u8, &str)> {
let hashes = line.bytes().take_while(|&b| b == b'#').count();
if hashes == 0 || hashes > 6 {
return None;
}
let rest = &line[hashes..];
if !rest.is_empty() && !rest.starts_with([' ', '\t']) {
return None;
}
let text = rest.trim();
let trimmed = text.trim_end_matches('#');
let text = if trimmed.len() == text.len() {
text
} else if trimmed.is_empty() || trimmed.ends_with([' ', '\t']) {
trimmed.trim_end()
} else {
text
};
Some((hashes as u8, text))
}
fn setext_underline(line: &str) -> Option<u8> {
let t = line.trim_end();
if t.is_empty() {
return None;
}
if t.bytes().all(|b| b == b'=') {
Some(1)
} else if t.bytes().all(|b| b == b'-') {
Some(2)
} else {
None
}
}
fn is_thematic_break(line: &str) -> bool {
let s: String = line.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'_'))
}
fn fence_start(line: &str) -> Option<(u8, usize, String)> {
let b = line.as_bytes();
let ch = *b.first()?;
if ch != b'`' && ch != b'~' {
return None;
}
let len = line.bytes().take_while(|&c| c == ch).count();
if len < 3 {
return None;
}
let info = line[len..].trim();
if ch == b'`' && info.contains('`') {
return None;
}
Some((ch, len, linkref::unescape_string(info)))
}
fn is_closing_fence(line: &str, ch: u8, open_len: usize) -> bool {
let len = line.bytes().take_while(|&c| c == ch).count();
len >= open_len && line[len..].trim().is_empty()
}
const HTML_BLOCK_TAGS: &[&str] = &[
"address",
"article",
"aside",
"base",
"basefont",
"blockquote",
"body",
"caption",
"center",
"col",
"colgroup",
"dd",
"details",
"dialog",
"dir",
"div",
"dl",
"dt",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"frame",
"frameset",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"head",
"header",
"hr",
"html",
"iframe",
"legend",
"li",
"link",
"main",
"menu",
"menuitem",
"nav",
"noframes",
"ol",
"optgroup",
"option",
"p",
"param",
"search",
"section",
"summary",
"table",
"tbody",
"td",
"tfoot",
"th",
"thead",
"title",
"tr",
"track",
"ul",
];
fn contains_ci(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return true;
}
let h = haystack.as_bytes();
let n = needle.as_bytes();
if h.len() < n.len() {
return false;
}
(0..=h.len() - n.len()).any(|i| {
h[i..i + n.len()]
.iter()
.zip(n)
.all(|(a, b)| a.eq_ignore_ascii_case(b))
})
}
fn html_block_start(line: &str, in_paragraph: bool) -> Option<HtmlEnd> {
let b = line.as_bytes();
if b.first() != Some(&b'<') {
return None;
}
for (tag, close) in [
("script", "</script>"),
("pre", "</pre>"),
("style", "</style>"),
("textarea", "</textarea>"),
] {
if starts_tag_ci(line, tag) {
let after = &line[1 + tag.len()..];
if after.is_empty() || after.starts_with([' ', '\t', '>']) {
return Some(HtmlEnd::Marker(close));
}
}
}
if line.starts_with("<!--") {
return Some(HtmlEnd::Marker("-->"));
}
if line.starts_with("<?") {
return Some(HtmlEnd::Marker("?>"));
}
if line.starts_with("<![CDATA[") {
return Some(HtmlEnd::Marker("]]>"));
}
if b.get(1) == Some(&b'!') && b.get(2).is_some_and(|c| c.is_ascii_alphabetic()) {
return Some(HtmlEnd::Marker(">"));
}
let (rest, _closing) = match b.get(1) {
Some(b'/') => (&line[2..], true),
_ => (&line[1..], false),
};
for tag in HTML_BLOCK_TAGS {
if starts_word_ci(rest, tag) {
let after = &rest[tag.len()..];
if after.is_empty() || after.starts_with([' ', '\t', '>']) || after.starts_with("/>") {
return Some(HtmlEnd::Blank);
}
}
}
if !in_paragraph {
if let Some(after) = complete_tag(line) {
if after.trim().is_empty() {
return Some(HtmlEnd::Blank);
}
}
}
None
}
fn starts_tag_ci(line: &str, tag: &str) -> bool {
let b = line.as_bytes();
b.first() == Some(&b'<') && starts_word_ci(&line[1..], tag)
}
fn starts_word_ci(s: &str, word: &str) -> bool {
let b = s.as_bytes();
let w = word.as_bytes();
b.len() >= w.len()
&& b[..w.len()]
.iter()
.zip(w)
.all(|(a, c)| a.eq_ignore_ascii_case(c))
}
fn complete_tag(line: &str) -> Option<&str> {
let b = line.as_bytes();
let end = if b.get(1) == Some(&b'/') {
crate::inline::scan_closing_tag(b, 0)?
} else {
let (e, name) = crate::inline::scan_open_tag(b, 0)?;
let lname = name.to_ascii_lowercase();
if matches!(lname.as_str(), "script" | "style" | "pre" | "textarea") {
return None;
}
e
};
Some(&line[end..])
}
fn list_marker(line: &str) -> Option<Marker> {
let b = line.as_bytes();
if let Some(&c) = b.first() {
if c == b'-' || c == b'*' || c == b'+' {
match b.get(1) {
Some(b' ') | Some(b'\t') | None => {
return Some(Marker {
ordered: false,
marker: c as char,
start: 1,
after: 1,
});
}
_ => {}
}
}
}
let digits = line.bytes().take_while(|c| c.is_ascii_digit()).count();
if (1..=9).contains(&digits) {
let sep = b.get(digits).copied();
if sep == Some(b'.') || sep == Some(b')') {
match b.get(digits + 1) {
Some(b' ') | Some(b'\t') | None => {
let start: u64 = line[..digits].parse().unwrap_or(1);
return Some(Marker {
ordered: true,
marker: sep.unwrap() as char,
start,
after: digits + 1,
});
}
_ => {}
}
}
}
None
}
fn split_row(line: &str) -> Vec<String> {
let mut s = line.trim();
s = s.strip_prefix('|').unwrap_or(s);
s = s.strip_suffix('|').unwrap_or(s);
let mut cells = Vec::new();
let mut cur = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => {
if let Some(&n) = chars.peek() {
cur.push('\\');
cur.push(n);
chars.next();
} else {
cur.push('\\');
}
}
'|' => {
cells.push(cur.trim().to_string());
cur.clear();
}
_ => cur.push(c),
}
}
cells.push(cur.trim().to_string());
cells
}
fn parse_delim_row(line: &str) -> Option<Vec<Alignment>> {
if !line.contains('|') && !line.contains('-') {
return None;
}
let cells = split_row(line);
if cells.is_empty() {
return None;
}
let mut aligns = Vec::with_capacity(cells.len());
for cell in &cells {
let c = cell.trim();
if c.is_empty() {
return None;
}
let left = c.starts_with(':');
let right = c.ends_with(':');
let mid = &c[usize::from(left)..c.len() - usize::from(right)];
if mid.is_empty() || !mid.bytes().all(|b| b == b'-') {
return None;
}
aligns.push(match (left, right) {
(true, true) => Alignment::Center,
(true, false) => Alignment::Left,
(false, true) => Alignment::Right,
(false, false) => Alignment::None,
});
}
Some(aligns)
}
fn parse_refdef(b: &[u8], i: usize) -> Option<(String, LinkDef, usize)> {
if b.get(i) != Some(&b'[') {
return None;
}
let mut j = i + 1;
let mut label = String::new();
loop {
match b.get(j) {
Some(b'\\') if b.get(j + 1).is_some_and(|c| c.is_ascii_punctuation()) => {
label.push('\\');
label.push(b[j + 1] as char);
j += 2;
}
Some(b']') => break,
Some(b'[') => return None,
Some(&c) if c < 0x80 => {
label.push(c as char);
j += 1;
}
Some(_) => {
let s = String::from_utf8_lossy(&b[j..]);
let ch = s.chars().next()?;
label.push(ch);
j += ch.len_utf8();
}
None => return None,
}
}
if b.get(j) != Some(&b']') || b.get(j + 1) != Some(&b':') {
return None;
}
j += 2;
j = skip_inline_ws_to_one_newline(b, j)?;
let (raw_dest, after_dest) = linkref::parse_destination(b, j)?;
j = after_dest;
let (title_ws, ws_newlines) = scan_ws(b, j);
let after_ws = title_ws;
let dest_line_end = line_end(b, j);
let mut def_title = String::new();
let end;
if after_ws > j && ws_newlines <= 1 {
if let Some((raw_title, after_title)) = linkref::parse_title(b, after_ws) {
let rest = skip_spaces(b, after_title);
if rest >= b.len() || b[rest] == b'\n' {
def_title = linkref::normalize_title(&raw_title);
end = if rest < b.len() { rest + 1 } else { rest };
} else {
end = dest_line_end?;
}
} else {
end = dest_line_end?;
}
} else {
end = dest_line_end?;
}
Some((
label,
LinkDef {
dest: linkref::normalize_dest(&raw_dest),
title: def_title,
},
end,
))
}
fn skip_inline_ws_to_one_newline(b: &[u8], mut i: usize) -> Option<usize> {
let mut newlines = 0;
while i < b.len() {
match b[i] {
b' ' | b'\t' | b'\r' => i += 1,
b'\n' => {
newlines += 1;
if newlines > 1 {
return None;
}
i += 1;
}
_ => break,
}
}
Some(i)
}
fn skip_spaces(b: &[u8], mut i: usize) -> usize {
while i < b.len() && matches!(b[i], b' ' | b'\t' | b'\r') {
i += 1;
}
i
}
fn scan_ws(b: &[u8], mut i: usize) -> (usize, usize) {
let mut nl = 0;
while i < b.len() {
match b[i] {
b' ' | b'\t' | b'\r' => i += 1,
b'\n' => {
nl += 1;
i += 1;
}
_ => break,
}
}
(i, nl)
}
fn line_end(b: &[u8], i: usize) -> Option<usize> {
let mut j = i;
while j < b.len() {
match b[j] {
b' ' | b'\t' | b'\r' => j += 1,
b'\n' => return Some(j + 1),
_ => return None,
}
}
Some(j)
}