markdown_stream/block.rs
1//! The streaming block parser: turns input lines into block-level events, deferring inline parsing
2//! until a paragraph/heading closes.
3//!
4//! CommonMark block structure with a proper **container stack**: a document holds a stack of open
5//! containers (block quotes, lists, list items) each anchored to a continuation *column*, plus at
6//! most one open *leaf* (paragraph, fenced/indented code, HTML block, table). Each input line is
7//! matched against the open containers by leading indentation (CommonMark "continuation" rules):
8//! a block quote continues with `>`, a list item's content continues at the column past its marker.
9//! Containers the line no longer belongs to are closed; new containers the line opens are pushed.
10//!
11//! Lists carry a tight/loose flag that is only knowable once the list closes (a blank line between
12//! items makes the list loose). To keep `EnterBlock(List{tight})` correct we **buffer a list's
13//! events** into the [`Container::List`] frame and replay them — correctly `<p>`-wrapped or not —
14//! when the list closes. The buffer is bounded by the list's own size, preserving bounded-memory
15//! streaming, and the looseness decision is a pure function of the (chunk-independent) line sequence,
16//! so split-equivalence still holds.
17
18use crate::event::*;
19use crate::inline;
20use crate::linkref;
21use crate::parser::Parser;
22use std::collections::HashMap;
23
24/// Expanded width of a tab stop, per CommonMark (tabs count to the next multiple of 4).
25const TAB: usize = 4;
26
27#[derive(Default)]
28pub struct StreamParser {
29 buf: Vec<u8>,
30 started: bool,
31 flushed: bool,
32 /// The open container stack (block quotes / lists / list items), outermost first. The document
33 /// itself is implicit (tracked by `started`).
34 containers: Vec<Container>,
35 /// The single open leaf block, if any (a paragraph, code block, …).
36 leaf: Leaf,
37 /// Was the previous processed line blank? Used for loose-list detection (a blank line between
38 /// two items, or before a second block in an item, makes the enclosing list loose).
39 last_blank: bool,
40 /// Link reference definitions seen so far, keyed by normalised label. Populated in line order as
41 /// paragraphs are scanned; references resolve against the definitions visible at close time.
42 refs: HashMap<String, LinkDef>,
43 /// When set, GFM extensions that are *not* part of CommonMark are enabled: extended (bare)
44 /// autolinks and task-list-item markers. (Strikethrough and tables are always on.) The flag is
45 /// off by default so the plain [`StreamParser::new`] path stays CommonMark-faithful.
46 gfm: bool,
47 /// The forward-reference output gate. Finalised top-level output is staged here as [`Slot`]s
48 /// (resolved events plus deferred inline runs) so a block holding an as-yet-undefined reference —
49 /// and every event after it — can be held until the reference resolves or `flush()` is reached,
50 /// while a document with no forward references streams out eagerly. See [`Self::drain_gate`].
51 gate: Vec<Slot>,
52}
53
54/// A staged unit of top-level output. Most output is a finalised [`Event`]; a block whose inline
55/// content references an as-yet-undefined label is staged as a [`Slot::Deferred`] run to be
56/// re-parsed once all link reference definitions are known.
57enum Slot {
58 /// A finalised event, replayed verbatim on release.
59 Event(Event),
60 /// A run of inline content holding one or more **forward references**: re-parsed against the
61 /// complete `refs` map at release time (so a later `[label]: /url` resolves it).
62 Deferred(Deferred),
63}
64
65/// A held inline run carrying at least one forward reference. `text` is the raw (refdef-stripped,
66/// trailing-trimmed) inline source; `style` the base style; `labels` the normalised labels that were
67/// undefined when the run was first parsed — once every one of them is either defined or known to be
68/// undefinable (only at `flush`), the run can be re-parsed and released.
69struct Deferred {
70 text: String,
71 style: InlineStyle,
72 labels: Vec<String>,
73}
74
75/// A list item's paragraph run when assembled at list close: either fully resolved events or a
76/// deferred (forward-reference-carrying) run to re-parse at gate release.
77enum ParaRun {
78 Resolved(Vec<Event>),
79 Deferred(Deferred),
80}
81
82/// One open container in the stack.
83enum Container {
84 /// A block quote. Its `>` marker is consumed during the match phase.
85 BlockQuote,
86 /// A list. Events for the whole list are buffered here until it closes, so the `tight` flag can
87 /// be back-patched once looseness is known.
88 List(ListFrame),
89 /// A single list item. `indent` is the column at which the item's content begins (the marker's
90 /// own indent plus its width plus the spaces after it): a continuation line must be indented at
91 /// least this far to stay in the item.
92 Item { indent: usize },
93}
94
95/// A buffered list: its metadata plus the events emitted while it is open, so the final `tight`
96/// flag (only known at close) can be applied to all item content retroactively.
97struct ListFrame {
98 ordered: bool,
99 marker: char,
100 start: u64,
101 /// `true` once any blank line is found that should make the list loose.
102 loose: bool,
103 /// Buffered events for the list body (everything between `EnterBlock(List)` and
104 /// `ExitBlock(List)`, exclusive). Item boundaries are marked so `<p>` wrappers can be inserted.
105 events: Vec<BufEvent>,
106 /// A blank line has been seen since the last block was added to this list, and no block has been
107 /// added since. If another block is then added (a sibling item, or a second block in the current
108 /// item), the list is loose. A trailing blank never commits, so it does not make the list loose.
109 pending_blank: bool,
110}
111
112/// An event buffered inside a list frame. Plain events pass through; `ItemStart`/`ItemEnd` mark item
113/// boundaries and `BlockSep` records where a `<p>` wrapper is needed in loose mode.
114enum BufEvent {
115 /// A raw event to replay verbatim.
116 Raw(Event),
117 /// Start of a list item's content (after `EnterBlock(ListItem)`).
118 ItemStart,
119 /// End of a list item's content (before `ExitBlock(ListItem)`).
120 ItemEnd,
121 /// A run of paragraph inline content (the text events between `<p>`…`</p>`), buffered so that in
122 /// a tight list the wrapper is dropped and in a loose list it is kept.
123 Para(Vec<Event>),
124 /// A deferred inline run (a list-item paragraph carrying a forward reference), with the same
125 /// looseness-dependent `<p>` wrapping as [`BufEvent::Para`] but re-parsed at gate-release time.
126 /// `prefix` holds any already-materialised leading events (e.g. a GFM task-list checkbox).
127 DeferPara {
128 prefix: Vec<Event>,
129 deferred: Deferred,
130 },
131 /// A deferred inline run propagated *as-is* from a nested list whose looseness wrapping was
132 /// already applied: replayed verbatim (like [`BufEvent::Raw`]) without re-wrapping.
133 DeferRaw(Deferred),
134}
135
136#[derive(Default)]
137enum Leaf {
138 #[default]
139 None,
140 Paragraph(String),
141 /// An indented code block. Lines are accumulated (already de-indented by 4 columns) and emitted
142 /// verbatim at close, with trailing blank lines trimmed.
143 Indented(Vec<String>),
144 Fenced {
145 ch: u8,
146 len: usize,
147 /// The indentation (in columns) of the opening fence; up to this much leading whitespace is
148 /// stripped from each content line.
149 indent: usize,
150 },
151 Table {
152 aligns: Vec<Alignment>,
153 },
154 /// A raw HTML block (one of the seven CommonMark start conditions). Content is emitted verbatim,
155 /// line by line, until `end` is satisfied.
156 Html {
157 end: HtmlEnd,
158 },
159}
160
161/// The end condition for an open HTML block, per the seven CommonMark start conditions. The string
162/// variants close on the *first line containing* the marker (inclusive); `Blank` closes on the first
163/// blank line (which is not part of the block).
164#[derive(Clone, Copy)]
165enum HtmlEnd {
166 /// Conditions 1–5: close on the first line that contains this (case-insensitive) marker.
167 Marker(&'static str),
168 /// Conditions 6–7: close on the first blank line.
169 Blank,
170}
171
172/// A list marker parsed from a line: bullet/ordered, its char, start number, and the byte offset of
173/// the first content character after the marker (and the spaces following it).
174struct Marker {
175 ordered: bool,
176 marker: char,
177 start: u64,
178 /// Byte offset (within the de-indented content) just past the marker char and its separator.
179 after: usize,
180}
181
182impl StreamParser {
183 /// A CommonMark parser (GFM-only extensions off).
184 pub fn new() -> Self {
185 Self::default()
186 }
187
188 /// A parser with GFM-only extensions enabled (extended autolinks, task-list items). Strikethrough
189 /// and tables are always recognised regardless of this flag.
190 pub fn new_gfm() -> Self {
191 StreamParser {
192 gfm: true,
193 ..Self::default()
194 }
195 }
196}
197
198impl Parser for StreamParser {
199 fn write(&mut self, chunk: &[u8]) -> Vec<Event> {
200 let mut out = Vec::new();
201 self.buf.extend_from_slice(chunk);
202 while let Some(nl) = self.buf.iter().position(|&b| b == b'\n') {
203 let mut line: Vec<u8> = self.buf.drain(..=nl).collect();
204 line.pop(); // drop '\n'
205 if line.last() == Some(&b'\r') {
206 line.pop();
207 }
208 let s = String::from_utf8_lossy(&line).into_owned();
209 self.process_line(&s, &mut out);
210 }
211 out
212 }
213
214 fn flush(&mut self) -> Vec<Event> {
215 let mut out = Vec::new();
216 if self.flushed {
217 return out;
218 }
219 if !self.buf.is_empty() {
220 let line = std::mem::take(&mut self.buf);
221 let s = String::from_utf8_lossy(&line).into_owned();
222 self.process_line(&s, &mut out);
223 }
224 self.close_leaf(&mut out);
225 self.close_containers_to(0, &mut out);
226 if self.started {
227 // Stage the document close behind any still-held content, then force a final drain: all
228 // link reference definitions are now known, so every remaining deferred run resolves
229 // (or, if still undefined, falls back to literal text).
230 self.gate
231 .push(Slot::Event(Event::exit(BlockKind::Document)));
232 }
233 self.flush_gate(&mut out);
234 self.flushed = true;
235 out
236 }
237
238 fn reset(&mut self) {
239 *self = Self::default();
240 }
241}
242
243impl StreamParser {
244 fn ensure_doc(&mut self, out: &mut Vec<Event>) {
245 if !self.started {
246 self.emit(out, Event::enter(BlockKind::Document));
247 self.started = true;
248 }
249 }
250
251 /// Process one logical line. This is the CommonMark block algorithm in three phases:
252 /// 1. **Match** the line against open containers, consuming each container's continuation
253 /// marker and tracking the surviving content offset/column.
254 /// 2. Decide whether unmatched containers close (they do, unless a lazy paragraph continuation
255 /// keeps a paragraph alive).
256 /// 3. **Parse** new containers and the leaf from the remaining content.
257 fn process_line(&mut self, raw: &str, out: &mut Vec<Event>) {
258 // Phase 1: walk the container stack, consuming continuation markers.
259 let mut cur = Cursor::new(raw);
260 let mut matched = 0usize; // number of containers whose continuation matched
261 for c in &self.containers {
262 match c {
263 Container::BlockQuote => {
264 let save = cur.clone();
265 if cur.indent() <= 3 && cur.peek_nonspace() == Some(b'>') {
266 cur.advance_to_nonspace();
267 cur.bump(); // consume '>'
268 if cur.peek() == Some(b' ') {
269 cur.bump();
270 } else if cur.peek() == Some(b'\t') {
271 cur.consume_tab_as_space();
272 }
273 matched += 1;
274 } else {
275 cur = save;
276 break;
277 }
278 }
279 Container::List(_) => {
280 // A list as such has no continuation marker; its item does.
281 matched += 1;
282 }
283 Container::Item { indent } => {
284 if cur.is_blank() {
285 // A blank line "matches" any item (it may continue the item with later,
286 // sufficiently-indented content). Stop consuming further markers.
287 matched += 1;
288 // Keep matching outer list frames is moot; break out.
289 break;
290 }
291 if cur.indent() >= *indent {
292 cur.consume_cols(*indent);
293 matched += 1;
294 } else {
295 break;
296 }
297 }
298 }
299 }
300
301 let all_matched = matched == self.containers.len();
302 let blank = cur.is_blank();
303
304 // Phase 2 + 3: dispatch. Fenced/HTML/table leaves swallow lines specially.
305 self.dispatch(raw, cur, matched, all_matched, blank, out);
306 self.last_blank = blank;
307 }
308
309 /// The continuation of `process_line` after the container-match phase: handle the open leaf's
310 /// special swallowing, blank lines, lazy continuation, new containers, and new leaves.
311 fn dispatch(
312 &mut self,
313 raw: &str,
314 mut cur: Cursor,
315 matched: usize,
316 all_matched: bool,
317 blank: bool,
318 out: &mut Vec<Event>,
319 ) {
320 // --- Open fenced code block: literal lines until the closing fence. ---
321 if let Leaf::Fenced { ch, len, indent } = self.leaf {
322 if all_matched {
323 let t = cur.rest_str();
324 let tt = t.trim_start();
325 if is_closing_fence(tt, ch, len) {
326 self.close_leaf(out);
327 } else {
328 // Strip up to `indent` columns of leading whitespace from the content line.
329 let stripped = strip_cols(&t, indent);
330 self.emit(out, Event::text(format!("{stripped}\n")));
331 }
332 return;
333 }
334 // The fence's container was interrupted: close the leaf and re-handle the line below.
335 self.close_leaf(out);
336 self.close_containers_to(matched, out);
337 }
338
339 // --- Open HTML block: emit verbatim until the end condition. ---
340 if let Leaf::Html { end } = self.leaf {
341 if all_matched {
342 let content = cur.rest_str();
343 match end {
344 HtmlEnd::Marker(marker) => {
345 self.emit(out, Event::text(format!("{content}\n")));
346 if contains_ci(&content, marker) {
347 self.close_leaf(out);
348 }
349 return;
350 }
351 HtmlEnd::Blank => {
352 if content.trim().is_empty() {
353 self.close_leaf(out);
354 // fall through: blank line handled below
355 } else {
356 self.emit(out, Event::text(format!("{content}\n")));
357 return;
358 }
359 }
360 }
361 if !matches!(self.leaf, Leaf::None) {
362 return;
363 }
364 } else {
365 self.close_leaf(out);
366 self.close_containers_to(matched, out);
367 }
368 }
369
370 // --- Open table: a pipe row continues it; otherwise it closes. ---
371 if let Leaf::Table { aligns } = &self.leaf {
372 if all_matched && !blank {
373 let content = cur.rest_str();
374 if content.contains('|') {
375 let aligns = aligns.clone();
376 self.emit_row(split_row(&content), &aligns, out);
377 return;
378 }
379 }
380 self.close_leaf(out);
381 if !all_matched {
382 self.close_containers_to(matched, out);
383 }
384 }
385
386 // --- Open indented code block: 4-space continuation, or blank lines (kept). ---
387 if let Leaf::Indented(_) = &self.leaf {
388 if all_matched && (blank || cur.indent() >= TAB) {
389 if blank {
390 if let Leaf::Indented(lines) = &mut self.leaf {
391 lines.push(String::new());
392 }
393 return;
394 }
395 cur.consume_cols(TAB);
396 let line = cur.rest_str_with_partial_tab();
397 if let Leaf::Indented(lines) = &mut self.leaf {
398 lines.push(line);
399 }
400 return;
401 }
402 self.close_leaf(out);
403 if !all_matched {
404 self.close_containers_to(matched, out);
405 }
406 }
407
408 // --- Blank line handling. ---
409 if blank {
410 // A blank line closes any open paragraph.
411 if matches!(self.leaf, Leaf::Paragraph(_)) {
412 self.close_leaf(out);
413 }
414 // Record a pending blank on the innermost open list: if content later resumes in the
415 // same item or a sibling item appears (i.e. a new block is added to the list), the list
416 // becomes loose. A trailing blank with no following content never commits, so it does
417 // not make the list loose.
418 self.note_blank_in_item();
419 return;
420 }
421
422 // Lazy paragraph continuation: a paragraph survives even when outer containers didn't match,
423 // *provided* the un-matched remainder is ordinary paragraph text (not a new block start).
424 let lazy = !all_matched
425 && matches!(self.leaf, Leaf::Paragraph(_))
426 && self.can_lazily_continue(&cur);
427 if !lazy && !all_matched {
428 self.close_leaf(out);
429 self.close_containers_to(matched, out);
430 }
431
432 // Now open any *new* containers (block quotes / list items) the line introduces, looping so
433 // a line like `> - x` or `- - x` opens several at once.
434 if !lazy {
435 self.open_new_containers(&mut cur, out);
436 }
437
438 // If, after matching and opening, the innermost container is a *bare* list (its items all
439 // closed and no new item opened), a non-item block is landing at the list's level — so the
440 // list ends. E.g. `- a\n\n<!-- -->` closes the list before the HTML block.
441 if !lazy && matches!(self.containers.last(), Some(Container::List(_))) {
442 let keep = self.containers.len() - 1;
443 self.close_containers_to(keep, out);
444 }
445
446 // Finally, the remaining content forms (or continues) a leaf.
447 self.parse_leaf(&cur, raw, lazy, out);
448 }
449
450 /// Open block-quote and list-item containers introduced by the current line, advancing `cur`
451 /// past each marker. Loops to handle several markers on one line (`> - x`, `- - x`).
452 fn open_new_containers(&mut self, cur: &mut Cursor, out: &mut Vec<Event>) {
453 loop {
454 // ≥4 columns of leading space is an indented-code amount, not a container marker — it
455 // belongs to the leaf. Stop opening containers.
456 let indent = cur.indent();
457 if indent >= TAB {
458 break;
459 }
460
461 // Block quote?
462 if indent <= 3 && cur.peek_nonspace() == Some(b'>') {
463 self.ensure_doc(out);
464 self.close_leaf(out);
465 // A block quote opening directly inside a list item is a (second) block of that item,
466 // so a preceding blank line makes the enclosing list loose.
467 if matches!(self.containers.last(), Some(Container::Item { .. })) {
468 self.commit_pending_blank();
469 }
470 self.emit(out, Event::enter(BlockKind::BlockQuote));
471 self.containers.push(Container::BlockQuote);
472 cur.advance_to_nonspace();
473 cur.bump();
474 if cur.peek() == Some(b' ') {
475 cur.bump();
476 } else if cur.peek() == Some(b'\t') {
477 cur.consume_tab_as_space();
478 }
479 continue;
480 }
481
482 // List item? A thematic break takes precedence over a bullet marker, so `* * *` and
483 // `- - -` are horizontal rules, not one-item lists. (The break is parsed by `parse_leaf`.)
484 if indent <= 3 && !is_thematic_break(&cur.rest_after_indent()) {
485 if let Some(m) = self.parse_marker(cur) {
486 self.start_list_item(cur, m, out);
487 continue;
488 }
489 }
490 break;
491 }
492 }
493
494 /// Parse a list marker at the cursor (which sits at ≤3 spaces of indent), validating that it can
495 /// start/continue a list here (an ordered marker may only interrupt a paragraph if it starts at
496 /// `1`). Returns the marker, leaving `cur` unchanged on failure.
497 fn parse_marker(&self, cur: &Cursor) -> Option<Marker> {
498 let rest = cur.rest_after_indent();
499 let m = list_marker(&rest)?;
500 // The interrupt restrictions only apply when a marker would interrupt a *running-text*
501 // paragraph (not one already inside a list): a bullet or `1.` may interrupt, but an empty
502 // marker (`-` then EOL) or an ordered marker that doesn't start at 1 may not. Inside a list,
503 // these markers freely begin sibling items (e.g. `- foo\n-\n- bar`, `2. x` after `1) y`).
504 if matches!(self.leaf, Leaf::Paragraph(_)) && !self.in_any_list() {
505 let empty = rest[m.after..].trim().is_empty();
506 if empty || (m.ordered && m.start != 1) {
507 return None;
508 }
509 }
510 Some(m)
511 }
512
513 /// Open a list (if needed) and a new item for marker `m`, advancing `cur` past the marker and the
514 /// spaces that establish the item's content column.
515 fn start_list_item(&mut self, cur: &mut Cursor, m: Marker, out: &mut Vec<Event>) {
516 self.ensure_doc(out);
517 self.close_leaf(out);
518
519 // The item's content indent is stored *relative to the parent container's content column*
520 // (the column the cursor sits at on entry, after outer block-quote/item markers were
521 // consumed), because container matching consumes that many additional columns.
522 let base_col = cur.col();
523 // Advance past the leading indent and the marker characters.
524 cur.advance_to_nonspace();
525 cur.consume_bytes(m.after);
526 let marker_width = cur.col() - base_col; // leading indent + marker chars
527
528 // Spaces after the marker determine the content indent. 1–4 spaces → that many; ≥5 spaces or
529 // a tab means only one space counts and the rest is part of an indented code block; an empty
530 // marker (then EOL) gives one space of padding.
531 let spaces = cur.count_spaces();
532 let content_indent;
533 if cur.is_blank_from_here() {
534 // Empty item: marker immediately followed by end of line.
535 content_indent = marker_width + 1;
536 } else if (1..=TAB).contains(&spaces) {
537 content_indent = marker_width + spaces;
538 cur.consume_cols_max(spaces);
539 } else {
540 // ≥5 spaces (or tab): only one space is the marker padding; the remainder is code.
541 content_indent = marker_width + 1;
542 cur.consume_cols_max(1);
543 }
544
545 // Should this marker extend the current list or start a new one? At this point the deeper
546 // items the line did not match have already been closed, so the top container is the list
547 // this marker is a sibling of (if any). It extends iff that top container is a List of the
548 // *same* kind (ordered-ness + marker char); a *different* marker char at the same level
549 // begins a new sibling list, so the old one is closed first.
550 let same_list = matches!(self.containers.last(), Some(Container::List(l))
551 if l.ordered == m.ordered && l.marker == m.marker);
552 let diff_list = matches!(self.containers.last(), Some(Container::List(_))) && !same_list;
553
554 if diff_list {
555 // Close the sibling list of the other kind (e.g. `-` items followed by a `+` item).
556 let keep = self.containers.len() - 1;
557 self.close_containers_to(keep, out);
558 }
559
560 if same_list {
561 // Continuing the same list: a blank line before this sibling item makes the list loose.
562 self.commit_pending_blank();
563 } else {
564 // Starting a fresh list. If it is nested directly inside a list item, it is a second
565 // block of that item, so a blank line preceding it makes the *enclosing* list loose
566 // (e.g. `1. foo\n\n - bar`).
567 if matches!(self.containers.last(), Some(Container::Item { .. })) {
568 self.commit_pending_blank();
569 }
570 let frame = ListFrame {
571 ordered: m.ordered,
572 marker: m.marker,
573 start: m.start,
574 loose: false,
575 events: Vec::new(),
576 pending_blank: false,
577 };
578 self.containers.push(Container::List(frame));
579 }
580
581 self.emit(out, Event::enter(BlockKind::ListItem));
582 self.mark_item_start();
583 self.containers.push(Container::Item {
584 indent: content_indent,
585 });
586 }
587
588 /// Build (or continue) the leaf from the cursor's remaining content. `lazy` marks a lazy
589 /// paragraph continuation (no new containers were opened).
590 fn parse_leaf(&mut self, cur: &Cursor, raw: &str, lazy: bool, out: &mut Vec<Event>) {
591 let _ = raw;
592 let content = cur.rest_str();
593 let trimmed = content.trim_start();
594 let indent = cur.indent();
595
596 if lazy {
597 // Pure paragraph continuation. Trailing spaces are kept (a two-space run signals a hard
598 // line break, resolved during inline scanning); the final line's trailing run is trimmed
599 // when the paragraph closes.
600 if let Leaf::Paragraph(p) = &mut self.leaf {
601 p.push('\n');
602 p.push_str(&content);
603 }
604 return;
605 }
606
607 // A new block is about to be added inside a list item. If no leaf is currently open, this is a
608 // *second* block within the item (the first having closed, e.g. across a blank line); commit
609 // any pending blank so the enclosing list becomes loose.
610 if matches!(self.leaf, Leaf::None)
611 && matches!(self.containers.last(), Some(Container::Item { .. }))
612 {
613 self.commit_pending_blank();
614 }
615
616 // A ≥4-column-indented line while a paragraph is open can only continue it (indented code
617 // cannot interrupt a paragraph), so append and stop — no block re-parsing.
618 if indent >= TAB {
619 if let Leaf::Paragraph(p) = &mut self.leaf {
620 p.push('\n');
621 p.push_str(&content);
622 return;
623 }
624 // Otherwise it is an indented code block.
625 self.ensure_doc(out);
626 let mut c = cur.clone();
627 c.consume_cols(TAB);
628 let line = c.rest_str_with_partial_tab();
629 self.leaf = Leaf::Indented(vec![line]);
630 return;
631 }
632
633 // From here, work with the de-indented content (≤3 spaces stripped).
634 let in_paragraph = matches!(self.leaf, Leaf::Paragraph(_));
635
636 // HTML block start.
637 if let Some(end) = html_block_start(trimmed, in_paragraph) {
638 self.ensure_doc(out);
639 self.close_leaf(out);
640 // Raw-text blocks (`<script>`/`<style>`/`<pre>`/`<textarea>`) are exempt from the GFM tag
641 // filter; mark them so the renderer can tell them from other (filtered) HTML blocks.
642 let html_raw_text = matches!(
643 end,
644 HtmlEnd::Marker("</script>" | "</style>" | "</pre>" | "</textarea>")
645 );
646 self.emit(
647 out,
648 Event::EnterBlock {
649 block: BlockKind::HtmlBlock,
650 data: BlockData {
651 html_raw_text,
652 ..Default::default()
653 },
654 span: Span::default(),
655 },
656 );
657 self.emit(out, Event::text(format!("{trimmed}\n")));
658 match end {
659 HtmlEnd::Marker(marker) if contains_ci(trimmed, marker) => {
660 self.close_leaf(out);
661 }
662 _ => self.leaf = Leaf::Html { end },
663 }
664 return;
665 }
666
667 // Fenced code start.
668 if let Some((ch, len, info)) = fence_start(trimmed) {
669 self.ensure_doc(out);
670 self.close_leaf(out);
671 let data = BlockData {
672 info,
673 ..Default::default()
674 };
675 self.emit(
676 out,
677 Event::EnterBlock {
678 block: BlockKind::FencedCode,
679 data,
680 span: Span::default(),
681 },
682 );
683 self.leaf = Leaf::Fenced { ch, len, indent };
684 return;
685 }
686
687 // ATX heading.
688 if let Some((level, htext)) = atx_heading(trimmed) {
689 self.ensure_doc(out);
690 self.close_leaf(out);
691 let data = BlockData {
692 level,
693 ..Default::default()
694 };
695 self.emit(
696 out,
697 Event::EnterBlock {
698 block: BlockKind::Heading,
699 data,
700 span: Span::default(),
701 },
702 );
703 self.parse_inline(htext, out);
704 self.emit(out, Event::exit(BlockKind::Heading));
705 return;
706 }
707
708 // Setext heading underline: a `=`/`-` run directly under a paragraph turns it into a heading.
709 if let Leaf::Paragraph(_) = &self.leaf {
710 if let Some(level) = setext_underline(trimmed) {
711 if let Leaf::Paragraph(text) = std::mem::take(&mut self.leaf) {
712 let body = self.consume_refdefs(&text);
713 if body.is_empty() {
714 // The paragraph was only refdefs; the underline becomes its own thing —
715 // restore and fall through to thematic-break / paragraph handling.
716 self.leaf = Leaf::None;
717 } else {
718 let data = BlockData {
719 level,
720 ..Default::default()
721 };
722 self.emit(
723 out,
724 Event::EnterBlock {
725 block: BlockKind::Heading,
726 data,
727 span: Span::default(),
728 },
729 );
730 self.parse_inline(body.trim_end(), out);
731 self.emit(out, Event::exit(BlockKind::Heading));
732 return;
733 }
734 }
735 }
736 }
737
738 // Thematic break (checked after setext so `---` under a paragraph is a setext h2).
739 if is_thematic_break(trimmed) {
740 self.ensure_doc(out);
741 self.close_leaf(out);
742 self.emit(out, Event::enter(BlockKind::ThematicBreak));
743 self.emit(out, Event::exit(BlockKind::ThematicBreak));
744 return;
745 }
746
747 // Default: paragraph text (new or continuation).
748 match &mut self.leaf {
749 Leaf::Paragraph(p) => {
750 // A single-line paragraph containing `|` followed by a delimiter row starts a table.
751 if !p.contains('\n') && p.contains('|') {
752 if let Some(aligns) = parse_delim_row(trimmed) {
753 let headers = split_row(p);
754 if headers.len() == aligns.len() {
755 let header = std::mem::take(p);
756 self.leaf = Leaf::None;
757 self.start_table(&header, aligns, out);
758 return;
759 }
760 }
761 }
762 p.push('\n');
763 p.push_str(&content);
764 }
765 _ => {
766 // Blank remaining content (e.g. an empty list marker line `- `) opens no
767 // paragraph; the item simply waits for content on a following line.
768 if trimmed.is_empty() {
769 return;
770 }
771 self.ensure_doc(out);
772 // Keep the first line's trailing spaces (a hard-break signal); leading whitespace was
773 // already stripped by `trimmed`.
774 self.leaf = Leaf::Paragraph(trimmed.to_string());
775 }
776 }
777 }
778
779 /// Can the current line lazily continue an open paragraph? It can unless its remaining content
780 /// would start a new block (list/quote/heading/fence/thematic break/html). This is a conservative
781 /// check matching the cases the spec forbids from lazy continuation.
782 fn can_lazily_continue(&self, cur: &Cursor) -> bool {
783 let rest = cur.rest_after_indent();
784 let trimmed = rest.trim_start();
785 if trimmed.is_empty() {
786 return false;
787 }
788 if cur.indent() >= TAB {
789 // Indented enough to be code — but code can't interrupt a paragraph, so it *is* lazy text.
790 return true;
791 }
792 if is_thematic_break(trimmed) {
793 return false;
794 }
795 if atx_heading(trimmed).is_some() {
796 return false;
797 }
798 if fence_start(trimmed).is_some() {
799 return false;
800 }
801 if cur.indent() <= 3 && cur.peek_nonspace() == Some(b'>') {
802 return false;
803 }
804 if html_block_start(trimmed, true).is_some() {
805 return false;
806 }
807 // A list marker interrupts a paragraph only if non-empty and (for ordered) starts at 1 —
808 // *unless* a list is already open, in which case any marker (even an empty one, or an ordered
809 // marker not starting at 1) starts a sibling item (e.g. `- foo\n-\n- bar`, `2. bar\n3) baz`).
810 if let Some(m) = list_marker(trimmed) {
811 if self.in_any_list() {
812 return false;
813 }
814 let empty = trimmed[m.after..].trim().is_empty();
815 if !(empty || (m.ordered && m.start != 1)) {
816 return false;
817 }
818 }
819 true
820 }
821
822 // --- list buffering helpers ---------------------------------------------------------------
823
824 /// Emit an event, routing it into the innermost open list's buffer if one is open, else staging
825 /// it on the forward-reference gate. `out` is drained from the gate once per write/flush.
826 fn emit(&mut self, out: &mut Vec<Event>, ev: Event) {
827 if let Some(frame) = self.innermost_list_mut() {
828 frame.events.push(BufEvent::Raw(ev));
829 } else {
830 self.gate.push(Slot::Event(ev));
831 self.drain_gate(out);
832 }
833 }
834
835 /// Emit a buffered run of paragraph inline content (so the `<p>` wrapper can be toggled by
836 /// looseness at close time).
837 fn emit_para(&mut self, out: &mut Vec<Event>, para: Vec<Event>) {
838 if let Some(frame) = self.innermost_list_mut() {
839 frame.events.push(BufEvent::Para(para));
840 } else {
841 // Not in a list: paragraphs are always wrapped.
842 self.gate
843 .push(Slot::Event(Event::enter(BlockKind::Paragraph)));
844 for ev in para {
845 self.gate.push(Slot::Event(ev));
846 }
847 self.gate
848 .push(Slot::Event(Event::exit(BlockKind::Paragraph)));
849 self.drain_gate(out);
850 }
851 }
852
853 /// Buffer a deferred list-item paragraph run (a direct child of a list item) so its `<p>` wrapper
854 /// can be toggled by looseness at list close, like [`BufEvent::Para`]. Only called while a list
855 /// is open.
856 fn emit_buf_para_defer(&mut self, prefix: Vec<Event>, deferred: Deferred) {
857 if let Some(frame) = self.innermost_list_mut() {
858 frame.events.push(BufEvent::DeferPara { prefix, deferred });
859 }
860 }
861
862 /// Buffer a deferred inline run that is already positioned between explicit `<p>` events (a
863 /// paragraph nested under another block inside a list item): replayed verbatim, no re-wrapping.
864 /// Only called while a list is open.
865 fn emit_buf_raw_defer(&mut self, deferred: Deferred) {
866 if let Some(frame) = self.innermost_list_mut() {
867 frame.events.push(BufEvent::DeferRaw(deferred));
868 }
869 }
870
871 /// Stage a deferred paragraph inline run (one carrying a forward reference) at the top level: a
872 /// `<p>` wrapper around an optional already-materialised `prefix` and the deferred run.
873 fn emit_defer_para(&mut self, out: &mut Vec<Event>, prefix: Vec<Event>, deferred: Deferred) {
874 self.gate
875 .push(Slot::Event(Event::enter(BlockKind::Paragraph)));
876 for ev in prefix {
877 self.gate.push(Slot::Event(ev));
878 }
879 self.gate.push(Slot::Deferred(deferred));
880 self.gate
881 .push(Slot::Event(Event::exit(BlockKind::Paragraph)));
882 self.drain_gate(out);
883 }
884
885 // --- forward-reference output gate --------------------------------------------------------
886
887 /// Release as many staged [`Slot`]s as is safe, preserving document order. Leading `Slot::Event`s
888 /// flow out freely; a `Slot::Deferred` flows out only once **every** label it awaits is defined
889 /// (re-parsed against the now-complete-enough `refs`). The walk stops at the first deferred slot
890 /// still awaiting a definition — holding it, and everything after it, until the definition lands
891 /// or `flush()` forces a final drain. This bounds buffering to the span from the first unresolved
892 /// reference to its resolving definition (or EOF), and emits nothing out of order.
893 fn drain_gate(&mut self, out: &mut Vec<Event>) {
894 let mut release = 0;
895 for slot in &self.gate {
896 match slot {
897 Slot::Event(_) => release += 1,
898 Slot::Deferred(d) => {
899 if d.labels.iter().all(|l| self.refs.contains_key(l)) {
900 release += 1;
901 } else {
902 break;
903 }
904 }
905 }
906 }
907 let released: Vec<Slot> = self.gate.drain(..release).collect();
908 self.emit_slots(released, out);
909 }
910
911 /// Force-release every staged slot at end of input: all link reference definitions are now known,
912 /// so any deferred run whose labels are still undefined re-parses to literal text (CommonMark).
913 fn flush_gate(&mut self, out: &mut Vec<Event>) {
914 let slots: Vec<Slot> = std::mem::take(&mut self.gate);
915 self.emit_slots(slots, out);
916 }
917
918 /// Materialise a run of released slots into `out`, re-parsing deferred runs against `self.refs`.
919 fn emit_slots(&self, slots: Vec<Slot>, out: &mut Vec<Event>) {
920 for slot in slots {
921 match slot {
922 Slot::Event(ev) => out.push(ev),
923 Slot::Deferred(d) => {
924 inline::parse(&d.text, &d.style, &self.refs, self.gfm, out);
925 }
926 }
927 }
928 }
929
930 fn mark_item_start(&mut self) {
931 if let Some(frame) = self.innermost_list_mut() {
932 frame.events.push(BufEvent::ItemStart);
933 }
934 }
935
936 fn mark_item_end(&mut self) {
937 if let Some(frame) = self.innermost_list_mut() {
938 frame.events.push(BufEvent::ItemEnd);
939 }
940 }
941
942 /// Has the current (innermost) list item received no content yet? True iff the last buffered
943 /// event for the open list is the `ItemStart` marker — used to recognise a *first*-block task
944 /// marker.
945 fn item_is_empty(&mut self) -> bool {
946 matches!(
947 self.innermost_list_mut().and_then(|f| f.events.last()),
948 Some(BufEvent::ItemStart)
949 )
950 }
951
952 /// Record that a blank line occurred while inside a list. Walking outward from the innermost
953 /// container, mark each open list with a pending blank — but stop at the first block quote: a
954 /// blank line inside a nested block quote belongs to that quote and must not make a list *outside*
955 /// the quote loose (the quote "absorbs" the blank). If a later block lands in one of the marked
956 /// lists, that list is loose.
957 fn note_blank_in_item(&mut self) {
958 for c in self.containers.iter_mut().rev() {
959 match c {
960 Container::List(l) => l.pending_blank = true,
961 Container::BlockQuote => break,
962 Container::Item { .. } => {}
963 }
964 }
965 }
966
967 /// Commit a pending blank: a new block is being added to the innermost open list, so if a blank
968 /// preceded it that list becomes loose. The blank is then "consumed" — its flag is cleared on
969 /// *every* open list, so a blank that separates blocks of an inner list does not also make an
970 /// enclosing list loose (the enclosing list is only loose if a blank directly precedes one of its
971 /// own added blocks).
972 fn commit_pending_blank(&mut self) {
973 let mut committed = false;
974 for c in self.containers.iter_mut().rev() {
975 if let Container::List(l) = c {
976 if !committed {
977 if l.pending_blank {
978 l.loose = true;
979 }
980 committed = true;
981 }
982 l.pending_blank = false;
983 }
984 }
985 }
986
987 /// The innermost open list frame (mutable), or `None` if no list is open.
988 fn innermost_list_mut(&mut self) -> Option<&mut ListFrame> {
989 for c in self.containers.iter_mut().rev() {
990 if let Container::List(l) = c {
991 return Some(l);
992 }
993 }
994 None
995 }
996
997 /// Whether any list is currently open (so a closing item's content gets buffered).
998 fn in_any_list(&self) -> bool {
999 self.containers
1000 .iter()
1001 .any(|c| matches!(c, Container::List(_)))
1002 }
1003
1004 /// Whether a closing paragraph is a *direct* child of a list item (the innermost open container
1005 /// is an `Item`). Only such paragraphs participate in tight/loose `<p>`-stripping; a paragraph
1006 /// nested under, say, a block quote inside the item is always wrapped.
1007 fn para_is_direct_list_child(&self) -> bool {
1008 matches!(self.containers.last(), Some(Container::Item { .. }))
1009 }
1010
1011 // --- container closing --------------------------------------------------------------------
1012
1013 /// Close (and emit) all containers above index `keep`, deepest first.
1014 fn close_containers_to(&mut self, keep: usize, out: &mut Vec<Event>) {
1015 while self.containers.len() > keep {
1016 self.close_leaf(out);
1017 match self.containers.pop().unwrap() {
1018 Container::BlockQuote => {
1019 self.emit(out, Event::exit(BlockKind::BlockQuote));
1020 }
1021 Container::Item { .. } => {
1022 self.mark_item_end();
1023 self.emit(out, Event::exit(BlockKind::ListItem));
1024 }
1025 Container::List(frame) => {
1026 self.flush_list(frame, out);
1027 }
1028 }
1029 }
1030 }
1031
1032 /// Replay a finished list's buffered events to `out` (or to the enclosing list's buffer if this
1033 /// list was nested), applying the resolved `tight`/`loose` decision to wrap (or not) each item's
1034 /// paragraph content in `<p>`.
1035 fn flush_list(&mut self, frame: ListFrame, out: &mut Vec<Event>) {
1036 let tight = !frame.loose;
1037 let data = BlockData {
1038 list: Some(ListData {
1039 ordered: frame.ordered,
1040 start: frame.start,
1041 tight,
1042 marker: frame.marker,
1043 }),
1044 ..Default::default()
1045 };
1046
1047 // Assemble the body. Each item's child events are first collected flat (wrapped paragraphs
1048 // expanded), then run through `wrap_item_children`, which applies the CommonMark `<li>`
1049 // newline rules: a `\n` precedes every top-level *block* child of the item, and a `\n`
1050 // precedes `</li>` iff the item's last child is a block. Inline children (tight, unwrapped
1051 // paragraph text) get no separators, so a tight inline-only item stays `<li>x</li>`.
1052 let mut body: Vec<Slot> = Vec::new();
1053 let mut item: Vec<Slot> = Vec::new();
1054 let mut in_item = false;
1055
1056 // Push a paragraph run (resolved events or a deferred run) into `target`, wrapping it in
1057 // `<p>` only when the list is loose. `prefix` holds any already-materialised leading events.
1058 let push_para = |target: &mut Vec<Slot>, prefix: Vec<Event>, run: ParaRun| {
1059 if !tight {
1060 target.push(Slot::Event(Event::enter(BlockKind::Paragraph)));
1061 }
1062 for ev in prefix {
1063 target.push(Slot::Event(ev));
1064 }
1065 match run {
1066 ParaRun::Resolved(inner) => target.extend(inner.into_iter().map(Slot::Event)),
1067 ParaRun::Deferred(d) => target.push(Slot::Deferred(d)),
1068 }
1069 if !tight {
1070 target.push(Slot::Event(Event::exit(BlockKind::Paragraph)));
1071 }
1072 };
1073
1074 for be in frame.events {
1075 match be {
1076 BufEvent::ItemStart => {
1077 in_item = true;
1078 item.clear();
1079 }
1080 BufEvent::ItemEnd => {
1081 let wrapped = wrap_item_children(std::mem::take(&mut item));
1082 body.extend(wrapped);
1083 in_item = false;
1084 }
1085 BufEvent::Para(inner) => {
1086 let target = if in_item { &mut item } else { &mut body };
1087 push_para(target, Vec::new(), ParaRun::Resolved(inner));
1088 }
1089 BufEvent::DeferPara { prefix, deferred } => {
1090 let target = if in_item { &mut item } else { &mut body };
1091 push_para(target, prefix, ParaRun::Deferred(deferred));
1092 }
1093 BufEvent::DeferRaw(deferred) => {
1094 let target = if in_item { &mut item } else { &mut body };
1095 target.push(Slot::Deferred(deferred));
1096 }
1097 BufEvent::Raw(ev) => {
1098 let target = if in_item { &mut item } else { &mut body };
1099 target.push(Slot::Event(ev));
1100 }
1101 }
1102 }
1103
1104 // Route the assembled list either to the enclosing list buffer or straight to the gate.
1105 if let Some(parent) = self.innermost_list_mut() {
1106 parent.events.push(BufEvent::Raw(Event::EnterBlock {
1107 block: BlockKind::List,
1108 data,
1109 span: Span::default(),
1110 }));
1111 for slot in body {
1112 match slot {
1113 Slot::Event(ev) => parent.events.push(BufEvent::Raw(ev)),
1114 // Already positioned by this list's looseness: propagate as-is (no re-wrapping).
1115 Slot::Deferred(deferred) => parent.events.push(BufEvent::DeferRaw(deferred)),
1116 }
1117 }
1118 parent
1119 .events
1120 .push(BufEvent::Raw(Event::exit(BlockKind::List)));
1121 } else {
1122 self.gate.push(Slot::Event(Event::EnterBlock {
1123 block: BlockKind::List,
1124 data,
1125 span: Span::default(),
1126 }));
1127 self.gate.extend(body);
1128 self.gate.push(Slot::Event(Event::exit(BlockKind::List)));
1129 self.drain_gate(out);
1130 }
1131 }
1132
1133 // --- leaf closing -------------------------------------------------------------------------
1134
1135 fn close_leaf(&mut self, out: &mut Vec<Event>) {
1136 match std::mem::take(&mut self.leaf) {
1137 Leaf::None => {}
1138 Leaf::Paragraph(text) => {
1139 let body = self.consume_refdefs(&text);
1140 // The final line's trailing whitespace is not significant (only *interior* line-end
1141 // whitespace can form a hard break), so trim the very end before inline parsing.
1142 let body = body.trim_end();
1143 if body.is_empty() {
1144 return;
1145 }
1146 // GFM task-list item: a list item whose first block is a paragraph beginning with
1147 // `[ ]`, `[x]`, or `[X]` (followed by whitespace) renders a checkbox in place of the
1148 // marker. Detect it only at the item's first content. The checkbox is a pre-built
1149 // event that precedes the (possibly deferred) inline content.
1150 let mut prefix = Vec::new();
1151 let mut body = body;
1152 if self.gfm && self.para_is_direct_list_child() && self.item_is_empty() {
1153 if let Some((checked, rest)) = task_marker(body) {
1154 prefix.push(Event::Text {
1155 text: format!(
1156 "<input {}disabled=\"\" type=\"checkbox\"> ",
1157 if checked { "checked=\"\" " } else { "" }
1158 ),
1159 style: InlineStyle {
1160 raw_html: true,
1161 ..Default::default()
1162 },
1163 span: Span::default(),
1164 });
1165 body = rest;
1166 }
1167 }
1168 // Parse the inline content, collecting any forward references (labels not yet
1169 // defined). When some surface, hold this paragraph's content as a deferred run so it
1170 // re-parses once the definitions are known; otherwise emit it eagerly as before.
1171 let style = InlineStyle::default();
1172 let mut inner = Vec::new();
1173 let mut labels = Vec::new();
1174 inline::parse_collect_unresolved(
1175 body,
1176 &style,
1177 &self.refs,
1178 self.gfm,
1179 &mut inner,
1180 &mut labels,
1181 );
1182 let deferred = (!labels.is_empty()).then(|| Deferred {
1183 text: body.to_string(),
1184 style,
1185 labels,
1186 });
1187
1188 if self.para_is_direct_list_child() {
1189 // A direct child of a list item: buffer so the list's looseness can decide on the
1190 // `<p>` wrapper later (tight → no wrapper).
1191 match deferred {
1192 Some(deferred) => self.emit_buf_para_defer(prefix, deferred),
1193 None => {
1194 let mut run = prefix;
1195 run.extend(inner);
1196 self.emit_para(out, run);
1197 }
1198 }
1199 } else if self.in_any_list() {
1200 // Inside a list but nested under another block (e.g. a blockquote in the item):
1201 // always wrapped, but routed through the list buffer to preserve order.
1202 self.emit(out, Event::enter(BlockKind::Paragraph));
1203 for ev in prefix {
1204 self.emit(out, ev);
1205 }
1206 match deferred {
1207 Some(deferred) => self.emit_buf_raw_defer(deferred),
1208 None => {
1209 for ev in inner {
1210 self.emit(out, ev);
1211 }
1212 }
1213 }
1214 self.emit(out, Event::exit(BlockKind::Paragraph));
1215 } else {
1216 // Top level: stage on the gate (held iff deferred).
1217 match deferred {
1218 Some(deferred) => self.emit_defer_para(out, prefix, deferred),
1219 None => {
1220 let mut run = prefix;
1221 run.extend(inner);
1222 self.emit_para(out, run);
1223 }
1224 }
1225 }
1226 }
1227 Leaf::Indented(mut lines) => {
1228 // Trim trailing blank lines.
1229 while lines.last().map(|l| l.trim().is_empty()) == Some(true) {
1230 lines.pop();
1231 }
1232 if lines.is_empty() {
1233 return;
1234 }
1235 self.emit(out, Event::enter(BlockKind::IndentedCode));
1236 let mut text = lines.join("\n");
1237 text.push('\n');
1238 self.emit(out, Event::text(text));
1239 self.emit(out, Event::exit(BlockKind::IndentedCode));
1240 }
1241 Leaf::Fenced { .. } => {
1242 self.emit(out, Event::exit(BlockKind::FencedCode));
1243 }
1244 Leaf::Table { .. } => {
1245 self.emit(out, Event::exit(BlockKind::Table));
1246 }
1247 Leaf::Html { .. } => {
1248 self.emit(out, Event::exit(BlockKind::HtmlBlock));
1249 }
1250 }
1251 }
1252
1253 /// Parse inline content (a heading or table cell, already positioned between its block's
1254 /// enter/exit events) into the innermost list buffer or onto the gate. A forward reference holds
1255 /// the run as a deferred unit so the surrounding block emits in order but the inline content is
1256 /// re-parsed once the definition is known.
1257 fn parse_inline(&mut self, text: &str, out: &mut Vec<Event>) {
1258 let style = InlineStyle::default();
1259 let mut inner = Vec::new();
1260 let mut labels = Vec::new();
1261 inline::parse_collect_unresolved(
1262 text,
1263 &style,
1264 &self.refs,
1265 self.gfm,
1266 &mut inner,
1267 &mut labels,
1268 );
1269 let deferred = (!labels.is_empty()).then(|| Deferred {
1270 text: text.to_string(),
1271 style,
1272 labels,
1273 });
1274 if self.in_any_list() {
1275 match deferred {
1276 Some(deferred) => self.emit_buf_raw_defer(deferred),
1277 None => {
1278 for ev in inner {
1279 self.emit(out, ev);
1280 }
1281 }
1282 }
1283 } else {
1284 match deferred {
1285 Some(deferred) => {
1286 self.gate.push(Slot::Deferred(deferred));
1287 self.drain_gate(out);
1288 }
1289 None => {
1290 for ev in inner {
1291 self.emit(out, ev);
1292 }
1293 }
1294 }
1295 }
1296 }
1297
1298 /// Strip leading link reference definitions from a buffered paragraph, registering each into
1299 /// `self.refs` (first definition of a label wins). Returns the remaining paragraph text (the
1300 /// lines after the last consumed definition), trimmed of the leading newline.
1301 ///
1302 /// A definition may span multiple buffered lines (the title may sit on a continuation line), so
1303 /// this works on the whole buffer rather than line-by-line. Parsing stops at the first position
1304 /// that does not begin a valid definition; everything from there on is paragraph text.
1305 fn consume_refdefs(&mut self, text: &str) -> String {
1306 let b = text.as_bytes();
1307 let mut pos = 0;
1308 loop {
1309 let line_start = pos;
1310 let mut p = pos;
1311 let mut spaces = 0;
1312 while p < b.len() && b[p] == b' ' {
1313 spaces += 1;
1314 p += 1;
1315 }
1316 if spaces > 3 {
1317 break;
1318 }
1319 match parse_refdef(b, p) {
1320 Some((label, def, next)) => {
1321 if let Some(norm) = linkref::normalize_label(&label) {
1322 self.refs.entry(norm).or_insert(def);
1323 pos = next;
1324 } else {
1325 break;
1326 }
1327 }
1328 None => {
1329 pos = line_start;
1330 break;
1331 }
1332 }
1333 }
1334 text[pos..].to_string()
1335 }
1336
1337 fn start_table(&mut self, header: &str, aligns: Vec<Alignment>, out: &mut Vec<Event>) {
1338 let data = BlockData {
1339 alignment: aligns.clone(),
1340 ..Default::default()
1341 };
1342 self.emit(
1343 out,
1344 Event::EnterBlock {
1345 block: BlockKind::Table,
1346 data,
1347 span: Span::default(),
1348 },
1349 );
1350 self.emit_row(split_row(header), &aligns, out);
1351 self.leaf = Leaf::Table { aligns };
1352 }
1353
1354 fn emit_row(&mut self, mut cells: Vec<String>, aligns: &[Alignment], out: &mut Vec<Event>) {
1355 cells.resize(aligns.len(), String::new());
1356 self.emit(out, Event::enter(BlockKind::TableRow));
1357 for cell in cells {
1358 self.emit(out, Event::enter(BlockKind::TableCell));
1359 self.parse_inline(cell.trim(), out);
1360 self.emit(out, Event::exit(BlockKind::TableCell));
1361 }
1362 self.emit(out, Event::exit(BlockKind::TableRow));
1363 }
1364}
1365
1366// ---------------------------------------------------------------------------
1367// Cursor: a position within the current line tracking byte offset and virtual column (tabs → 4).
1368// ---------------------------------------------------------------------------
1369
1370/// A scanning cursor over one input line that tracks both a byte offset and a virtual *column*
1371/// (with tabs expanded to the next multiple of [`TAB`]). Container matching is column-based, so the
1372/// cursor lets a tab be partially consumed (e.g. a 4-wide tab where only 2 columns are needed).
1373#[derive(Clone)]
1374struct Cursor {
1375 bytes: Vec<u8>,
1376 /// Current byte offset.
1377 pos: usize,
1378 /// Current virtual column.
1379 column: usize,
1380 /// Columns of an in-progress tab already "consumed" (when a tab straddles a needed boundary).
1381 partial_tab: usize,
1382}
1383
1384impl Cursor {
1385 fn new(line: &str) -> Self {
1386 Cursor {
1387 bytes: line.as_bytes().to_vec(),
1388 pos: 0,
1389 column: 0,
1390 partial_tab: 0,
1391 }
1392 }
1393
1394 fn col(&self) -> usize {
1395 self.column
1396 }
1397
1398 fn peek(&self) -> Option<u8> {
1399 self.bytes.get(self.pos).copied()
1400 }
1401
1402 /// The next non-space/tab byte (without advancing).
1403 fn peek_nonspace(&self) -> Option<u8> {
1404 let mut i = self.pos;
1405 while i < self.bytes.len() && matches!(self.bytes[i], b' ' | b'\t') {
1406 i += 1;
1407 }
1408 self.bytes.get(i).copied()
1409 }
1410
1411 /// Columns of leading whitespace from the current position to the next non-space. The current
1412 /// column already reflects any partially-consumed tab, so `TAB - (col % TAB)` yields a tab's
1413 /// *remaining* width directly.
1414 fn indent(&self) -> usize {
1415 let mut col = self.column;
1416 let mut i = self.pos;
1417 let start = col;
1418 while i < self.bytes.len() {
1419 match self.bytes[i] {
1420 b' ' => {
1421 col += 1;
1422 i += 1;
1423 }
1424 b'\t' => {
1425 col += TAB - (col % TAB);
1426 i += 1;
1427 }
1428 _ => break,
1429 }
1430 }
1431 col - start
1432 }
1433
1434 /// Is the rest of the line blank (only whitespace)?
1435 fn is_blank(&self) -> bool {
1436 self.bytes[self.pos..]
1437 .iter()
1438 .all(|&b| matches!(b, b' ' | b'\t'))
1439 }
1440
1441 fn is_blank_from_here(&self) -> bool {
1442 self.is_blank()
1443 }
1444
1445 /// Advance one byte, updating the column. A tab advances to the next tab stop; if some of its
1446 /// columns were already consumed (`partial_tab`), `self.column` already reflects them, so the
1447 /// remaining width is simply `TAB - (column % TAB)`.
1448 fn bump(&mut self) {
1449 if let Some(b) = self.peek() {
1450 match b {
1451 b'\t' => {
1452 self.column += TAB - (self.column % TAB);
1453 self.partial_tab = 0;
1454 }
1455 _ => self.column += 1,
1456 }
1457 self.pos += 1;
1458 }
1459 }
1460
1461 /// Advance past `n` raw bytes (used for ASCII marker characters).
1462 fn consume_bytes(&mut self, n: usize) {
1463 for _ in 0..n {
1464 self.bump();
1465 }
1466 }
1467
1468 /// Skip leading spaces/tabs to the first non-whitespace byte.
1469 fn advance_to_nonspace(&mut self) {
1470 while matches!(self.peek(), Some(b' ') | Some(b'\t')) {
1471 self.bump();
1472 }
1473 }
1474
1475 /// Consume exactly `cols` columns of leading whitespace (splitting a tab if necessary). When a tab
1476 /// straddles the target column it is consumed partially: the byte is left in place but `column`
1477 /// (and `partial_tab`) advance, so a later read still sees the tab's remaining columns.
1478 fn consume_cols(&mut self, cols: usize) {
1479 let target = self.column + cols;
1480 while self.column < target {
1481 match self.peek() {
1482 Some(b' ') => self.bump(),
1483 Some(b'\t') => {
1484 // Remaining width of the (possibly already partially consumed) tab.
1485 let width = TAB - (self.column % TAB);
1486 if self.column + width <= target {
1487 self.bump();
1488 } else {
1489 let take = target - self.column;
1490 self.partial_tab += take;
1491 self.column = target;
1492 }
1493 }
1494 _ => break,
1495 }
1496 }
1497 }
1498
1499 /// Consume at most `cols` columns of leading whitespace.
1500 fn consume_cols_max(&mut self, cols: usize) {
1501 self.consume_cols(cols);
1502 }
1503
1504 /// Count columns of available leading whitespace (alias of [`indent`]).
1505 fn count_spaces(&self) -> usize {
1506 self.indent()
1507 }
1508
1509 /// A `&str` view of the rest of the line from the byte cursor.
1510 fn rest_str(&self) -> String {
1511 String::from_utf8_lossy(&self.bytes[self.pos..]).into_owned()
1512 }
1513
1514 /// The rest of the line after skipping the leading indent (≤ whatever spaces are present).
1515 fn rest_after_indent(&self) -> String {
1516 let mut i = self.pos;
1517 while i < self.bytes.len() && matches!(self.bytes[i], b' ' | b'\t') {
1518 i += 1;
1519 }
1520 String::from_utf8_lossy(&self.bytes[i..]).into_owned()
1521 }
1522
1523 /// Rest of the line, but if the cursor sits mid-tab (a tab whose leading columns were already
1524 /// consumed for column alignment), emit the tab's remaining columns as spaces before the rest.
1525 /// Used for code blocks, where the exact remaining indentation must be preserved verbatim.
1526 fn rest_str_with_partial_tab(&self) -> String {
1527 if self.partial_tab > 0 && self.peek() == Some(b'\t') {
1528 let remaining = TAB - (self.column % TAB);
1529 let mut s = " ".repeat(remaining);
1530 s.push_str(&String::from_utf8_lossy(&self.bytes[self.pos + 1..]));
1531 return s;
1532 }
1533 self.rest_str()
1534 }
1535
1536 /// Consume a single tab as if it were one space (for blockquote `> \t` padding).
1537 fn consume_tab_as_space(&mut self) {
1538 if self.peek() == Some(b'\t') {
1539 let width = TAB - (self.column % TAB);
1540 if width <= 1 {
1541 self.bump();
1542 } else {
1543 self.partial_tab += 1;
1544 self.column += 1;
1545 }
1546 }
1547 }
1548}
1549
1550// ---------------------------------------------------------------------------
1551// line classifiers
1552// ---------------------------------------------------------------------------
1553
1554/// Strip up to `cols` columns of leading whitespace from `s`, returning the remainder. Tabs expand
1555/// to [`TAB`]-column stops; a tab straddling the boundary leaves its remaining columns as spaces.
1556fn strip_cols(s: &str, cols: usize) -> String {
1557 let b = s.as_bytes();
1558 let mut col = 0;
1559 let mut i = 0;
1560 while i < b.len() && col < cols {
1561 match b[i] {
1562 b' ' => {
1563 col += 1;
1564 i += 1;
1565 }
1566 b'\t' => {
1567 let width = TAB - (col % TAB);
1568 if col + width <= cols {
1569 col += width;
1570 i += 1;
1571 } else {
1572 // partial tab: keep the leftover as spaces
1573 let leftover = (col + width) - cols;
1574 let mut out = " ".repeat(leftover);
1575 out.push_str(&String::from_utf8_lossy(&b[i + 1..]));
1576 return out;
1577 }
1578 }
1579 _ => break,
1580 }
1581 }
1582 String::from_utf8_lossy(&b[i..]).into_owned()
1583}
1584
1585/// Block kinds that, as a list item's child, force the CommonMark `<li>` newline layout (a `\n`
1586/// before the child and, if it is the item's last child, before `</li>`).
1587fn is_block_enter(ev: &Event) -> bool {
1588 matches!(
1589 ev,
1590 Event::EnterBlock {
1591 block: BlockKind::List
1592 | BlockKind::BlockQuote
1593 | BlockKind::FencedCode
1594 | BlockKind::IndentedCode
1595 | BlockKind::Heading
1596 | BlockKind::ThematicBreak
1597 | BlockKind::HtmlBlock
1598 | BlockKind::Table
1599 | BlockKind::Paragraph,
1600 ..
1601 }
1602 )
1603}
1604
1605/// Insert the CommonMark `<li>` separator newlines into a list item's flat child-event stream.
1606///
1607/// A `\n` is emitted before a *top-level* block child (one whose `EnterBlock` sits at item depth 0)
1608/// **only when the previous top-level child was inline** (or this is the first child). Block children
1609/// already end their own output with a newline (the HTML renderer emits `</p>\n`, `</ul>\n`, …), so a
1610/// separator between two consecutive blocks would double it; the separator is only needed after the
1611/// `<li>` itself (leading block child) or after an inline run (`<li>a\n<ul>…`). Inline content (tight,
1612/// unwrapped paragraph text) needs no separators — a tight inline-only item stays `<li>text</li>`.
1613fn wrap_item_children(slots: Vec<Slot>) -> Vec<Slot> {
1614 let mut out = Vec::with_capacity(slots.len() + 2);
1615 let mut depth = 0i32;
1616 // `prev_block`: was the most recent top-level child a block? Starts `false` so a leading block
1617 // child gets its `\n` (the `<li>\n…` layout).
1618 let mut prev_block = false;
1619 for slot in slots {
1620 match &slot {
1621 Slot::Event(Event::EnterBlock { .. }) => {
1622 if depth == 0 {
1623 if matches!(&slot, Slot::Event(ev) if is_block_enter(ev)) {
1624 if !prev_block {
1625 out.push(Slot::Event(Event::text("\n")));
1626 }
1627 prev_block = true;
1628 } else {
1629 prev_block = false;
1630 }
1631 }
1632 depth += 1;
1633 out.push(slot);
1634 }
1635 Slot::Event(Event::ExitBlock { .. }) => {
1636 depth -= 1;
1637 out.push(slot);
1638 }
1639 // A deferred inline run, like any inline content, sits at depth 0 as a non-block child.
1640 _ => {
1641 if depth == 0 {
1642 prev_block = false;
1643 }
1644 out.push(slot);
1645 }
1646 }
1647 }
1648 out
1649}
1650
1651/// A GFM task-list marker at the start of `body`: `[ ]`, `[x]`, or `[X]` followed by a space or tab.
1652/// Returns `(checked, rest)` where `rest` is the body after the marker and its single separator
1653/// space, or `None` if no marker is present. The bracket content must be exactly one character.
1654fn task_marker(body: &str) -> Option<(bool, &str)> {
1655 let b = body.as_bytes();
1656 if b.first() != Some(&b'[') || b.get(2) != Some(&b']') {
1657 return None;
1658 }
1659 let checked = match b.get(1) {
1660 Some(b' ') => false,
1661 Some(b'x') | Some(b'X') => true,
1662 _ => return None,
1663 };
1664 // A whitespace separator (or end of line) must follow the closing bracket.
1665 match b.get(3) {
1666 Some(b' ') | Some(b'\t') => Some((checked, &body[4..])),
1667 None => Some((checked, "")),
1668 _ => None,
1669 }
1670}
1671
1672fn atx_heading(line: &str) -> Option<(u8, &str)> {
1673 let hashes = line.bytes().take_while(|&b| b == b'#').count();
1674 if hashes == 0 || hashes > 6 {
1675 return None;
1676 }
1677 let rest = &line[hashes..];
1678 if !rest.is_empty() && !rest.starts_with([' ', '\t']) {
1679 return None;
1680 }
1681 let text = rest.trim();
1682 // An optional closing sequence: a run of `#` that is either the whole text or preceded by a space
1683 // or tab is stripped (so `# foo #` → `foo`), but a `#` run welded to the preceding word is content
1684 // (`# foo#` → `foo#`). The remaining text is inline-parsed, so any escaped `\#` survives as `#`.
1685 let trimmed = text.trim_end_matches('#');
1686 let text = if trimmed.len() == text.len() {
1687 // No trailing `#` run at all.
1688 text
1689 } else if trimmed.is_empty() || trimmed.ends_with([' ', '\t']) {
1690 // The `#` run is the whole text, or is preceded by whitespace → it is a closing sequence.
1691 trimmed.trim_end()
1692 } else {
1693 // The `#` run is attached to a word → keep it as content.
1694 text
1695 };
1696 Some((hashes as u8, text))
1697}
1698
1699/// A setext underline: a line of only `=` (level 1) or only `-` (level 2), ≤3 leading spaces.
1700fn setext_underline(line: &str) -> Option<u8> {
1701 let t = line.trim_end();
1702 if t.is_empty() {
1703 return None;
1704 }
1705 if t.bytes().all(|b| b == b'=') {
1706 Some(1)
1707 } else if t.bytes().all(|b| b == b'-') {
1708 Some(2)
1709 } else {
1710 None
1711 }
1712}
1713
1714fn is_thematic_break(line: &str) -> bool {
1715 let s: String = line.chars().filter(|c| !c.is_whitespace()).collect();
1716 s.len() >= 3
1717 && (s.bytes().all(|b| b == b'-')
1718 || s.bytes().all(|b| b == b'*')
1719 || s.bytes().all(|b| b == b'_'))
1720}
1721
1722fn fence_start(line: &str) -> Option<(u8, usize, String)> {
1723 let b = line.as_bytes();
1724 let ch = *b.first()?;
1725 if ch != b'`' && ch != b'~' {
1726 return None;
1727 }
1728 let len = line.bytes().take_while(|&c| c == ch).count();
1729 if len < 3 {
1730 return None;
1731 }
1732 let info = line[len..].trim();
1733 if ch == b'`' && info.contains('`') {
1734 return None;
1735 }
1736 // The info string resolves backslash escapes and entity references to their literal value (it is
1737 // ordinary inline text), so e.g. ``` foo\+bar ``` / ``` föö ``` give a clean language.
1738 Some((ch, len, linkref::unescape_string(info)))
1739}
1740
1741fn is_closing_fence(line: &str, ch: u8, open_len: usize) -> bool {
1742 let len = line.bytes().take_while(|&c| c == ch).count();
1743 len >= open_len && line[len..].trim().is_empty()
1744}
1745
1746/// HTML block tag names for start condition 6.
1747const HTML_BLOCK_TAGS: &[&str] = &[
1748 "address",
1749 "article",
1750 "aside",
1751 "base",
1752 "basefont",
1753 "blockquote",
1754 "body",
1755 "caption",
1756 "center",
1757 "col",
1758 "colgroup",
1759 "dd",
1760 "details",
1761 "dialog",
1762 "dir",
1763 "div",
1764 "dl",
1765 "dt",
1766 "fieldset",
1767 "figcaption",
1768 "figure",
1769 "footer",
1770 "form",
1771 "frame",
1772 "frameset",
1773 "h1",
1774 "h2",
1775 "h3",
1776 "h4",
1777 "h5",
1778 "h6",
1779 "head",
1780 "header",
1781 "hr",
1782 "html",
1783 "iframe",
1784 "legend",
1785 "li",
1786 "link",
1787 "main",
1788 "menu",
1789 "menuitem",
1790 "nav",
1791 "noframes",
1792 "ol",
1793 "optgroup",
1794 "option",
1795 "p",
1796 "param",
1797 "search",
1798 "section",
1799 "summary",
1800 "table",
1801 "tbody",
1802 "td",
1803 "tfoot",
1804 "th",
1805 "thead",
1806 "title",
1807 "tr",
1808 "track",
1809 "ul",
1810];
1811
1812fn contains_ci(haystack: &str, needle: &str) -> bool {
1813 if needle.is_empty() {
1814 return true;
1815 }
1816 let h = haystack.as_bytes();
1817 let n = needle.as_bytes();
1818 if h.len() < n.len() {
1819 return false;
1820 }
1821 (0..=h.len() - n.len()).any(|i| {
1822 h[i..i + n.len()]
1823 .iter()
1824 .zip(n)
1825 .all(|(a, b)| a.eq_ignore_ascii_case(b))
1826 })
1827}
1828
1829fn html_block_start(line: &str, in_paragraph: bool) -> Option<HtmlEnd> {
1830 let b = line.as_bytes();
1831 if b.first() != Some(&b'<') {
1832 return None;
1833 }
1834
1835 for (tag, close) in [
1836 ("script", "</script>"),
1837 ("pre", "</pre>"),
1838 ("style", "</style>"),
1839 ("textarea", "</textarea>"),
1840 ] {
1841 if starts_tag_ci(line, tag) {
1842 let after = &line[1 + tag.len()..];
1843 if after.is_empty() || after.starts_with([' ', '\t', '>']) {
1844 return Some(HtmlEnd::Marker(close));
1845 }
1846 }
1847 }
1848
1849 if line.starts_with("<!--") {
1850 return Some(HtmlEnd::Marker("-->"));
1851 }
1852 if line.starts_with("<?") {
1853 return Some(HtmlEnd::Marker("?>"));
1854 }
1855 if line.starts_with("<![CDATA[") {
1856 return Some(HtmlEnd::Marker("]]>"));
1857 }
1858 if b.get(1) == Some(&b'!') && b.get(2).is_some_and(|c| c.is_ascii_alphabetic()) {
1859 return Some(HtmlEnd::Marker(">"));
1860 }
1861
1862 let (rest, _closing) = match b.get(1) {
1863 Some(b'/') => (&line[2..], true),
1864 _ => (&line[1..], false),
1865 };
1866 for tag in HTML_BLOCK_TAGS {
1867 if starts_word_ci(rest, tag) {
1868 let after = &rest[tag.len()..];
1869 if after.is_empty() || after.starts_with([' ', '\t', '>']) || after.starts_with("/>") {
1870 return Some(HtmlEnd::Blank);
1871 }
1872 }
1873 }
1874
1875 if !in_paragraph {
1876 if let Some(after) = complete_tag(line) {
1877 if after.trim().is_empty() {
1878 return Some(HtmlEnd::Blank);
1879 }
1880 }
1881 }
1882
1883 None
1884}
1885
1886fn starts_tag_ci(line: &str, tag: &str) -> bool {
1887 let b = line.as_bytes();
1888 b.first() == Some(&b'<') && starts_word_ci(&line[1..], tag)
1889}
1890
1891fn starts_word_ci(s: &str, word: &str) -> bool {
1892 let b = s.as_bytes();
1893 let w = word.as_bytes();
1894 b.len() >= w.len()
1895 && b[..w.len()]
1896 .iter()
1897 .zip(w)
1898 .all(|(a, c)| a.eq_ignore_ascii_case(c))
1899}
1900
1901fn complete_tag(line: &str) -> Option<&str> {
1902 let b = line.as_bytes();
1903 let end = if b.get(1) == Some(&b'/') {
1904 crate::inline::scan_closing_tag(b, 0)?
1905 } else {
1906 let (e, name) = crate::inline::scan_open_tag(b, 0)?;
1907 let lname = name.to_ascii_lowercase();
1908 if matches!(lname.as_str(), "script" | "style" | "pre" | "textarea") {
1909 return None;
1910 }
1911 e
1912 };
1913 Some(&line[end..])
1914}
1915
1916/// Parse a list marker at the start of `line` (already de-indented). Returns the marker kind, char,
1917/// start number, and the byte offset just past the marker+separator (before the spaces that follow).
1918fn list_marker(line: &str) -> Option<Marker> {
1919 let b = line.as_bytes();
1920 // Bullet: -, *, + followed by a space/tab or end of line.
1921 if let Some(&c) = b.first() {
1922 if c == b'-' || c == b'*' || c == b'+' {
1923 match b.get(1) {
1924 Some(b' ') | Some(b'\t') | None => {
1925 return Some(Marker {
1926 ordered: false,
1927 marker: c as char,
1928 start: 1,
1929 after: 1,
1930 });
1931 }
1932 _ => {}
1933 }
1934 }
1935 }
1936 // Ordered: 1–9 digits, then '.' or ')', then a space/tab or EOL.
1937 let digits = line.bytes().take_while(|c| c.is_ascii_digit()).count();
1938 if (1..=9).contains(&digits) {
1939 let sep = b.get(digits).copied();
1940 if sep == Some(b'.') || sep == Some(b')') {
1941 match b.get(digits + 1) {
1942 Some(b' ') | Some(b'\t') | None => {
1943 let start: u64 = line[..digits].parse().unwrap_or(1);
1944 return Some(Marker {
1945 ordered: true,
1946 marker: sep.unwrap() as char,
1947 start,
1948 after: digits + 1,
1949 });
1950 }
1951 _ => {}
1952 }
1953 }
1954 }
1955 None
1956}
1957
1958fn split_row(line: &str) -> Vec<String> {
1959 let mut s = line.trim();
1960 s = s.strip_prefix('|').unwrap_or(s);
1961 s = s.strip_suffix('|').unwrap_or(s);
1962 let mut cells = Vec::new();
1963 let mut cur = String::new();
1964 let mut chars = s.chars().peekable();
1965 while let Some(c) = chars.next() {
1966 match c {
1967 '\\' => {
1968 if let Some(&n) = chars.peek() {
1969 cur.push('\\');
1970 cur.push(n);
1971 chars.next();
1972 } else {
1973 cur.push('\\');
1974 }
1975 }
1976 '|' => {
1977 cells.push(cur.trim().to_string());
1978 cur.clear();
1979 }
1980 _ => cur.push(c),
1981 }
1982 }
1983 cells.push(cur.trim().to_string());
1984 cells
1985}
1986
1987fn parse_delim_row(line: &str) -> Option<Vec<Alignment>> {
1988 if !line.contains('|') && !line.contains('-') {
1989 return None;
1990 }
1991 let cells = split_row(line);
1992 if cells.is_empty() {
1993 return None;
1994 }
1995 let mut aligns = Vec::with_capacity(cells.len());
1996 for cell in &cells {
1997 let c = cell.trim();
1998 if c.is_empty() {
1999 return None;
2000 }
2001 let left = c.starts_with(':');
2002 let right = c.ends_with(':');
2003 let mid = &c[usize::from(left)..c.len() - usize::from(right)];
2004 if mid.is_empty() || !mid.bytes().all(|b| b == b'-') {
2005 return None;
2006 }
2007 aligns.push(match (left, right) {
2008 (true, true) => Alignment::Center,
2009 (true, false) => Alignment::Left,
2010 (false, true) => Alignment::Right,
2011 (false, false) => Alignment::None,
2012 });
2013 }
2014 Some(aligns)
2015}
2016
2017// ---------------------------------------------------------------------------
2018// link reference definitions (unchanged from M2b)
2019// ---------------------------------------------------------------------------
2020
2021/// Try to parse a single link reference definition `[label]: dest "title"` starting at byte `i`.
2022fn parse_refdef(b: &[u8], i: usize) -> Option<(String, LinkDef, usize)> {
2023 if b.get(i) != Some(&b'[') {
2024 return None;
2025 }
2026 let mut j = i + 1;
2027 let mut label = String::new();
2028 loop {
2029 match b.get(j) {
2030 Some(b'\\') if b.get(j + 1).is_some_and(|c| c.is_ascii_punctuation()) => {
2031 label.push('\\');
2032 label.push(b[j + 1] as char);
2033 j += 2;
2034 }
2035 Some(b']') => break,
2036 Some(b'[') => return None,
2037 Some(&c) if c < 0x80 => {
2038 label.push(c as char);
2039 j += 1;
2040 }
2041 Some(_) => {
2042 let s = String::from_utf8_lossy(&b[j..]);
2043 let ch = s.chars().next()?;
2044 label.push(ch);
2045 j += ch.len_utf8();
2046 }
2047 None => return None,
2048 }
2049 }
2050 if b.get(j) != Some(&b']') || b.get(j + 1) != Some(&b':') {
2051 return None;
2052 }
2053 j += 2;
2054
2055 j = skip_inline_ws_to_one_newline(b, j)?;
2056
2057 let (raw_dest, after_dest) = linkref::parse_destination(b, j)?;
2058 j = after_dest;
2059
2060 let (title_ws, ws_newlines) = scan_ws(b, j);
2061 let after_ws = title_ws;
2062
2063 let dest_line_end = line_end(b, j);
2064 let mut def_title = String::new();
2065 let end;
2066
2067 if after_ws > j && ws_newlines <= 1 {
2068 if let Some((raw_title, after_title)) = linkref::parse_title(b, after_ws) {
2069 let rest = skip_spaces(b, after_title);
2070 if rest >= b.len() || b[rest] == b'\n' {
2071 def_title = linkref::normalize_title(&raw_title);
2072 end = if rest < b.len() { rest + 1 } else { rest };
2073 } else {
2074 end = dest_line_end?;
2075 }
2076 } else {
2077 end = dest_line_end?;
2078 }
2079 } else {
2080 end = dest_line_end?;
2081 }
2082
2083 Some((
2084 label,
2085 LinkDef {
2086 dest: linkref::normalize_dest(&raw_dest),
2087 title: def_title,
2088 },
2089 end,
2090 ))
2091}
2092
2093fn skip_inline_ws_to_one_newline(b: &[u8], mut i: usize) -> Option<usize> {
2094 let mut newlines = 0;
2095 while i < b.len() {
2096 match b[i] {
2097 b' ' | b'\t' | b'\r' => i += 1,
2098 b'\n' => {
2099 newlines += 1;
2100 if newlines > 1 {
2101 return None;
2102 }
2103 i += 1;
2104 }
2105 _ => break,
2106 }
2107 }
2108 Some(i)
2109}
2110
2111fn skip_spaces(b: &[u8], mut i: usize) -> usize {
2112 while i < b.len() && matches!(b[i], b' ' | b'\t' | b'\r') {
2113 i += 1;
2114 }
2115 i
2116}
2117
2118fn scan_ws(b: &[u8], mut i: usize) -> (usize, usize) {
2119 let mut nl = 0;
2120 while i < b.len() {
2121 match b[i] {
2122 b' ' | b'\t' | b'\r' => i += 1,
2123 b'\n' => {
2124 nl += 1;
2125 i += 1;
2126 }
2127 _ => break,
2128 }
2129 }
2130 (i, nl)
2131}
2132
2133fn line_end(b: &[u8], i: usize) -> Option<usize> {
2134 let mut j = i;
2135 while j < b.len() {
2136 match b[j] {
2137 b' ' | b'\t' | b'\r' => j += 1,
2138 b'\n' => return Some(j + 1),
2139 _ => return None,
2140 }
2141 }
2142 Some(j)
2143}