use crate::ast::*;
use crate::parser::Parser;
use dmc_lexer::token::TokenKind;
impl<'eng, 'tokens> Parser<'eng, 'tokens> {
pub(super) fn parse_blockquote(&mut self) -> Node {
let span = self.current_span();
let para_span = self.current_span();
let mut children: Vec<Vec<Node>> = vec![Vec::new()];
let mut paragraphs: Vec<Vec<Node>> = vec![Vec::new()];
loop {
let line_markers = self.count_line_blockquote_markers();
if line_markers == 0 {
let starts_other_block = self.line_starts_other_block();
let top = children.len() - 1;
let lazy_eligible = !paragraphs[top].is_empty()
&& !starts_other_block
&& matches!(
self.peek_kind(),
Some(TokenKind::Text)
| Some(TokenKind::Emphasis(_, _))
| Some(TokenKind::Strikethrough)
| Some(TokenKind::CodeInlineOpen(_))
| Some(TokenKind::Whitespace(_))
| Some(TokenKind::Autolink(_))
| Some(TokenKind::EntityRef)
| Some(TokenKind::LinkOpen)
| Some(TokenKind::ImageMarker)
| Some(TokenKind::SetextUnderline(_))
| Some(TokenKind::ThematicBreak)
);
if !lazy_eligible {
break;
}
let inline = self.collect_inline_until_break();
let break_kind = self.peek_kind().cloned();
if matches!(break_kind, Some(TokenKind::SoftBreak) | Some(TokenKind::HardBreak)) {
self.advance();
}
if !inline.is_empty() {
paragraphs[top].push(Node::Text(Text { value: " ".into(), span: para_span.clone() }));
paragraphs[top].extend(inline);
}
continue;
}
while children.len() < line_markers {
children.push(Vec::new());
paragraphs.push(Vec::new());
}
let deepest_para_open = !paragraphs.last().is_some_and(|p| p.is_empty());
let shrink_blocked = line_markers < children.len() && deepest_para_open;
if !shrink_blocked {
while children.len() > line_markers {
Self::close_blockquote_level(&mut children, &mut paragraphs, ¶_span, &span);
}
}
self.consume_blockquote_markers(line_markers);
let top = children.len() - 1;
let after_marker_kind = match self.peek_kind() {
Some(k) => k.clone(),
None => break,
};
if matches!(after_marker_kind, TokenKind::LinkRefDef) {
self.advance();
if matches!(self.peek_kind(), Some(TokenKind::SoftBreak) | Some(TokenKind::HardBreak)) {
self.advance();
}
continue;
}
if matches!(after_marker_kind, TokenKind::UnorderedListMarker | TokenKind::OrderedListMarker(_)) {
if !paragraphs[top].is_empty() {
let para = std::mem::take(&mut paragraphs[top]);
children[top].push(Node::Paragraph(Paragraph { children: para, span: para_span.clone() }));
}
let ordered = matches!(after_marker_kind, TokenKind::OrderedListMarker(_));
let list = self.parse_list_in_blockquote(ordered, line_markers);
children[top].push(list);
continue;
}
let indented_code: Option<Node> = if matches!(after_marker_kind, TokenKind::Whitespace(_))
&& self.peek_leading_indent().is_some_and(|n| n >= 4)
{
Some(self.parse_indented_code_in_bq())
} else {
None
};
let block_node: Option<Node> = if indented_code.is_some() {
indented_code
} else {
match after_marker_kind {
TokenKind::Heading(_) => Some(self.parse_heading()),
TokenKind::ThematicBreak => {
let hr_span = self.current_span();
self.advance();
Some(Node::HorizontalRule(HorizontalRule { span: hr_span }))
},
TokenKind::CodeFenceOpen(_, _) => Some(self.parse_code_block_in_blockquote(line_markers)),
TokenKind::JsxOpenTagStart | TokenKind::JsxCloseTagStart => {
self.jsx_html_block_mode().map(|m| self.parse_html_block_from_jsx(m))
},
TokenKind::HtmlCommentOpen => Some(self.parse_html_comment_block()),
_ => None,
}
};
if let Some(node) = block_node {
if !paragraphs[top].is_empty() {
let para = std::mem::take(&mut paragraphs[top]);
children[top].push(Node::Paragraph(Paragraph { children: para, span: para_span.clone() }));
}
children[top].push(node);
if matches!(self.peek_kind(), Some(TokenKind::SoftBreak) | Some(TokenKind::HardBreak)) {
self.advance();
}
continue;
}
let inline = self.collect_inline_until_break();
let break_kind = self.peek_kind().cloned();
if matches!(break_kind, Some(TokenKind::SoftBreak) | Some(TokenKind::HardBreak)) {
self.advance();
}
if inline.is_empty() {
if !paragraphs[top].is_empty() {
let para = std::mem::take(&mut paragraphs[top]);
children[top].push(Node::Paragraph(Paragraph { children: para, span: para_span.clone() }));
}
continue;
}
if !paragraphs[top].is_empty() {
paragraphs[top].push(Node::Text(Text { value: " ".into(), span: para_span.clone() }));
}
paragraphs[top].extend(inline);
if matches!(break_kind, Some(TokenKind::HardBreak)) {
let para = std::mem::take(&mut paragraphs[top]);
children[top].push(Node::Paragraph(Paragraph { children: para, span: para_span.clone() }));
}
}
while children.len() > 1 {
Self::close_blockquote_level(&mut children, &mut paragraphs, ¶_span, &span);
}
let mut root = children.pop().unwrap();
let last_para = paragraphs.pop().unwrap();
if !last_para.is_empty() {
root.push(Node::Paragraph(Paragraph { children: last_para, span: para_span }));
}
Node::Blockquote(Blockquote { children: root, span })
}
fn parse_list_in_blockquote(&mut self, ordered: bool, bq_depth: usize) -> Node {
let span = self.current_span();
let start: Option<u32> = if ordered {
self.peek().and_then(|t| {
let digits: String = t.raw.chars().take_while(|c| c.is_ascii_digit()).collect();
digits.parse::<u32>().ok()
})
} else {
None
};
let mut items: Vec<Node> = Vec::new();
let mut first = true;
loop {
let line_start = self.pos;
if !first {
let saved = self.pos;
let next_count = self.count_line_blockquote_markers();
if next_count < bq_depth {
self.pos = saved;
break;
}
self.consume_blockquote_markers(bq_depth);
}
first = false;
let kind = match self.peek_kind() {
Some(k) => k,
None => break,
};
let want_marker = if ordered {
matches!(kind, TokenKind::OrderedListMarker(_))
} else {
matches!(kind, TokenKind::UnorderedListMarker)
};
if !want_marker {
if !first {
self.pos = line_start;
}
break;
}
let marker_raw = self.peek_raw().unwrap_or("");
let marker_raw_width = marker_raw.chars().count();
let marker_raw_has_ws = marker_raw.ends_with([' ', '\t']);
let following_ws = match self.tokens.get(self.pos + 1) {
Some(t) if matches!(t.kind, TokenKind::Whitespace(_)) => t.raw.chars().count(),
_ => 0,
};
let marker_width = if !marker_raw_has_ws && following_ws > 0 { marker_raw_width + 1 } else { marker_raw_width };
let extra_ws = match self.tokens.get(self.pos + 1) {
Some(t) if matches!(t.kind, TokenKind::Whitespace(_)) => {
let ws = t.raw.chars().count();
if !marker_raw_has_ws && marker_raw_width > 0 { ws.saturating_sub(1) } else { ws }
},
_ => 0,
};
let item_content_extra = if extra_ws >= 4 { 0 } else { extra_ws };
let content_floor = (if ordered { marker_width.max(3) } else { marker_width.max(2) }) + item_content_extra;
self.advance();
if ordered {
let is_text = matches!(self.peek_kind(), Some(TokenKind::Text));
let raw_opt: Option<&'tokens str> = if is_text { self.peek_raw() } else { None };
if let Some(raw) = raw_opt {
let trimmed = raw.strip_prefix('.').unwrap_or(raw).trim_start_matches([' ', '\t']);
if trimmed.is_empty() {
self.advance();
} else if trimmed.len() != raw.len() {
let pos = self.pos;
self.tokens[pos].raw = trimmed;
}
}
}
let mut item = self.parse_one_list_item(ordered);
if matches!(self.peek_kind(), Some(TokenKind::SoftBreak) | Some(TokenKind::HardBreak)) {
self.advance();
}
loop {
let saved = self.pos;
let line_markers = self.count_line_blockquote_markers();
if line_markers < bq_depth {
break;
}
self.consume_blockquote_markers(bq_depth);
if !matches!(self.peek_kind(), Some(TokenKind::SoftBreak) | Some(TokenKind::HardBreak)) {
self.pos = saved;
break;
}
self.advance();
let next_saved = self.pos;
let next_markers = self.count_line_blockquote_markers();
if next_markers < bq_depth {
self.pos = saved;
break;
}
self.consume_blockquote_markers(bq_depth);
let indent = self.peek_leading_indent().unwrap_or(0);
if indent < content_floor || indent >= content_floor + 4 {
self.pos = saved;
break;
}
self.advance();
let para_span = self.current_span();
let inline = self.collect_inline_until_break();
if inline.is_empty() {
self.pos = next_saved;
break;
}
Self::ensure_loose_item(&mut item, &span);
Self::append_to_item(&mut item, Node::Paragraph(Paragraph { children: inline, span: para_span }));
if matches!(self.peek_kind(), Some(TokenKind::SoftBreak) | Some(TokenKind::HardBreak)) {
self.advance();
}
}
items.push(item);
}
Node::List(List { ordered, start, children: items, span })
}
fn close_blockquote_level(
children: &mut Vec<Vec<Node>>,
paragraphs: &mut Vec<Vec<Node>>,
para_span: &duck_diagnostic::Span,
bq_span: &duck_diagnostic::Span,
) {
let mut inner_children = children.pop().unwrap();
let pending = paragraphs.pop().unwrap();
if !pending.is_empty() {
inner_children.push(Node::Paragraph(Paragraph { children: pending, span: para_span.clone() }));
}
let bq = Node::Blockquote(Blockquote { children: inner_children, span: bq_span.clone() });
let parent_idx = children.len() - 1;
children[parent_idx].push(bq);
}
pub(super) fn count_line_blockquote_markers(&self) -> usize {
let mut count = 0usize;
let mut i = self.pos;
while let Some(t) = self.tokens.get(i) {
match t.kind {
TokenKind::BlockQuoteMarker => {
count += 1;
i += 1;
},
TokenKind::Whitespace(_) => {
i += 1;
},
_ => break,
}
}
count
}
pub(super) fn consume_blockquote_markers(&mut self, n: usize) {
let mut taken = 0usize;
while taken < n {
if matches!(self.peek_kind(), Some(TokenKind::Whitespace(_))) {
self.advance();
}
if !matches!(self.peek_kind(), Some(TokenKind::BlockQuoteMarker)) {
break;
}
self.advance();
taken += 1;
}
if self.peek_leading_indent().is_some_and(|n| n < 4) {
self.advance();
}
}
pub(super) fn line_starts_other_block(&self) -> bool {
let mut i = self.pos;
if matches!(self.tokens.get(i).map(|t| &t.kind), Some(TokenKind::Whitespace(_))) {
i += 1;
}
matches!(
self.tokens.get(i).map(|t| &t.kind),
Some(TokenKind::Heading(_))
| Some(TokenKind::ThematicBreak)
| Some(TokenKind::BlockQuoteMarker)
| Some(TokenKind::UnorderedListMarker)
| Some(TokenKind::OrderedListMarker(_))
| Some(TokenKind::IndentedCodeLine)
| Some(TokenKind::CodeFenceOpen(_, _))
| Some(TokenKind::HtmlCommentOpen)
| Some(TokenKind::HtmlBlockOpen(_))
| Some(TokenKind::JsxOpenTagStart)
| Some(TokenKind::JsxCloseTagStart)
| Some(TokenKind::LinkRefDef)
)
}
pub(super) fn try_promote_text_list_marker(&mut self) -> bool {
let Some(tok) = self.peek() else {
return false;
};
if !matches!(tok.kind, TokenKind::Text) {
return false;
}
let has_space_after =
matches!(self.tokens.get(self.pos + 1).map(|t| &t.kind), Some(TokenKind::Whitespace(w)) if *w > 0);
if !has_space_after {
return false;
}
let raw = tok.raw;
let kind = match raw {
"-" | "*" | "+" => Some(TokenKind::UnorderedListMarker),
_ => {
let digits_end = raw.find(|c: char| !c.is_ascii_digit()).unwrap_or(raw.len());
if digits_end == 0 || digits_end + 1 != raw.len() {
None
} else {
match raw.as_bytes()[digits_end] {
b'.' => Some(TokenKind::OrderedListMarker(dmc_lexer::token::OrderedSep::Period)),
b')' => Some(TokenKind::OrderedListMarker(dmc_lexer::token::OrderedSep::Paren)),
_ => None,
}
}
},
};
if let Some(kind) = kind {
self.tokens[self.pos].kind = kind;
true
} else {
false
}
}
pub(super) fn try_continue_same_list_after_blankline(
&mut self,
ordered: bool,
indent: usize,
items: &mut [Node],
span: &duck_diagnostic::Span,
saw_blank_between_items: &mut bool,
) -> bool {
if !matches!(self.peek_kind(), Some(TokenKind::BlankLine)) {
return false;
}
let blank_pos = self.pos;
self.advance();
let ws_pos = match self.peek_kind() {
Some(TokenKind::Whitespace(_)) => Some(self.pos),
_ => None,
};
if ws_pos.is_some() {
self.advance();
}
let next_is_marker = match self.peek_kind() {
Some(TokenKind::UnorderedListMarker) if !ordered => true,
Some(TokenKind::OrderedListMarker(_)) if ordered => true,
_ => false,
};
let next_indent =
if let Some(pos) = ws_pos { self.tokens.get(pos).map(|t| t.raw.chars().count()).unwrap_or(0) } else { 0 };
let same_list_indent = if indent > 0 { next_indent == indent } else { next_indent <= 3 };
if !next_is_marker || !same_list_indent {
self.pos = blank_pos;
return false;
}
if let Some(pos) = ws_pos {
self.pos = pos;
}
*saw_blank_between_items = true;
if let Some(last) = items.last_mut() {
Self::ensure_loose_item(last, span);
}
true
}
}