Skip to main content

base_markdown/
event_parser.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Pull semantic events from compact source-backed block records.
5use crate::block_parser::{Blocks, Body};
6use crate::definitions::Definitions;
7use crate::inline_parser::InlineParser;
8use crate::stream::{Event, EventKind, StreamError, Tag, TagKind, Text};
9use crate::util::line_ending::terminal_ending_start;
10use crate::value_builder::ValueBuilder;
11use crate::{LineEnding, ParseOptions};
12use alloc::string::ToString;
13
14/// A pull producer of resolved semantic events. Preparation records block structure; inline syntax is scanned only when its containing block is consumed.
15pub struct EventParser<'a> {
16    blocks: Blocks,
17    source: &'a str,
18    options: &'a ParseOptions,
19    definitions: Definitions<'a>,
20    inline: InlineParser,
21    extensions: bool,
22    cursor: usize,
23    closing: bool,
24    phase: Phase,
25}
26impl<'a> EventParser<'a> {
27    /// Prepare Markdown for pull consumption.
28    /// Built-in Markdown preparation has no syntax errors.
29    pub fn new(source: &'a str, options: &'a ParseOptions) -> Self {
30        let (blocks, definitions) = Blocks::parse(source, options);
31        Self {
32            blocks,
33            source,
34            options,
35            definitions,
36            inline: InlineParser::default(),
37            extensions: options.constructs != crate::Constructs::default(),
38            cursor: 0,
39            closing: false,
40            phase: Phase::Start,
41        }
42    }
43}
44impl Iterator for EventParser<'_> {
45    type Item = Result<Event<'static>, StreamError>;
46    // Paired mixed-document benchmarks justify inlining this event boundary.
47    #[allow(clippy::inline_always)]
48    #[inline(always)]
49    fn next(&mut self) -> Option<Self::Item> {
50        self.next_event()
51    }
52}
53// Pending separators are delivery states, mutually exclusive with inline consumption.
54enum Phase {
55    Start,
56    Body,
57    Inline,
58    Separator(usize),
59    DocumentEnd,
60    Done,
61}
62impl EventParser<'_> {
63    fn prepare_inline<const EXTENSIONS: bool>(&mut self, index: usize, task: bool, table: bool) {
64        let fragments = self
65            .blocks
66            .children(index)
67            .map(|(_, node)| node.start..node.end);
68        if self.definitions.is_empty() {
69            self.inline.prepare::<false, EXTENSIONS>(
70                self.source,
71                fragments,
72                &self.definitions,
73                self.options,
74                task,
75                table,
76            );
77        } else {
78            self.inline.prepare::<true, EXTENSIONS>(
79                self.source,
80                fragments,
81                &self.definitions,
82                self.options,
83                task,
84                table,
85            );
86        }
87    }
88    fn advance(&mut self) {
89        let node = self.blocks.nodes[self.cursor];
90        if node.next != 0 {
91            self.cursor = node.next;
92            self.closing = false;
93        } else {
94            self.cursor = node.parent;
95            self.closing = true;
96        }
97    }
98    fn value(&self, index: usize) -> Text<'static> {
99        let mut value = ValueBuilder::new(self.source);
100        for (_, node) in self.blocks.children(index) {
101            let Body::Fragment(spaces) = node.body else {
102                unreachable!("literal block children are source fragments");
103            };
104            if spaces > 0 {
105                value.append(Text::Literal(&"   "[..spaces]));
106            }
107            value.append(Text::Source(node.start..node.end));
108        }
109        value.finish()
110    }
111    fn separator(&self, end: usize) -> Event<'static> {
112        let bytes = self.source.as_bytes();
113        // Block boundaries passed here end at a physical line ending, never at container padding or a trimmed inline fragment.
114        let start = match bytes[end - 1] {
115            b'\n' => end - 1 - usize::from(end > 1 && bytes[end - 2] == b'\r'),
116            b'\r' => end - 1,
117            _ => unreachable!("authored block separators end at a source newline"),
118        };
119        Event {
120            kind: EventKind::Separator {
121                value: Text::Source(start..end),
122                conditional: false,
123            },
124            span: end..end,
125        }
126    }
127    fn end_document(&mut self) -> Event<'static> {
128        self.phase = Phase::Done;
129        Event {
130            kind: EventKind::End(TagKind::Document),
131            span: self.source.len()..self.source.len(),
132        }
133    }
134    // Document bookends are infrequent; keep their state transitions out of the hot body loop.
135    #[cold]
136    fn document_boundary(&mut self) -> Option<Result<Event<'static>, StreamError>> {
137        match self.phase {
138            Phase::Start => {
139                self.phase = Phase::Body;
140                self.cursor = self.blocks.nodes[0].child;
141                self.closing = self.cursor == 0;
142                let line_ending = LineEnding::from_source(self.source);
143                Some(Ok(Event {
144                    kind: EventKind::Start(Tag::Document(line_ending)),
145                    span: 0..self.source.len(),
146                }))
147            }
148            Phase::DocumentEnd => Some(Ok(self.end_document())),
149            Phase::Done => None,
150            Phase::Body => {
151                self.phase = Phase::DocumentEnd;
152                let last = self.blocks.children(0).last().map_or(0, |(index, _)| index);
153                let mut leaf = last;
154                while matches!(
155                    self.blocks.nodes[leaf].body,
156                    Body::Quote | Body::List { .. } | Body::Item(_)
157                ) {
158                    let Some((child, _)) = self.blocks.children(leaf).last() else {
159                        break;
160                    };
161                    leaf = child;
162                }
163                let last = self.blocks.nodes[last];
164                let leaf = self.blocks.nodes[leaf];
165                let populated = matches!(
166                    leaf.body,
167                    Body::Paragraph
168                        | Body::Heading(_)
169                        | Body::Rule
170                        | Body::Code(_)
171                        | Body::Html(_)
172                        | Body::Table
173                );
174                let suppress_separator = match leaf.body {
175                    Body::Html(end) => end == leaf.end && leaf.end == self.source.len(),
176                    Body::Code(metadata) if leaf.parent == 0 => {
177                        let code = &self.blocks.codes[metadata];
178                        code.unclosed && leaf.end == self.source.len()
179                            || code.fence.is_none()
180                                && self.source[leaf.end..].trim().is_empty()
181                                && !self.source.ends_with(['\r', '\n'])
182                    }
183                    _ => false,
184                };
185                if !suppress_separator
186                    && self.blocks.nodes[0].child != 0
187                    && (!matches!(last.body, Body::Definition(_)) || last.end < self.source.len())
188                    && (self.source.ends_with(['\r', '\n'])
189                        || (matches!(
190                            last.body,
191                            Body::Paragraph | Body::Heading(_) | Body::Rule | Body::Table
192                        ) || last.body == Body::Quote && populated)
193                            && self.source[..last.end]
194                                .trim_end_matches([' ', '\t'])
195                                .ends_with(['\r', '\n']))
196                {
197                    // Populated containers retain their final semantic line ending. Empty containers and lists followed by blank lines use the document style.
198                    return Some(Ok(
199                        if !populated
200                            || matches!(last.body, Body::List { .. })
201                                && leaf.end != self.source.len()
202                            || leaf.body == Body::Table && last.end > leaf.end
203                        {
204                            Event {
205                                kind: EventKind::Separator {
206                                    value: Text::Literal(""),
207                                    conditional: true,
208                                },
209                                span: last.end..last.end,
210                            }
211                        } else {
212                            self.separator(leaf.end)
213                        },
214                    ));
215                }
216                Some(Ok(self.end_document()))
217            }
218            Phase::Inline | Phase::Separator(_) => {
219                unreachable!("active body delivery bypasses document boundary handling")
220            }
221        }
222    }
223    // Paired mixed-document benchmarks justify inlining this event boundary.
224    #[allow(clippy::inline_always)]
225    #[inline(always)]
226    fn next_event(&mut self) -> Option<Result<Event<'static>, StreamError>> {
227        match self.phase {
228            Phase::Inline => {
229                if let Some(event) = self
230                    .inline
231                    .next(self.source, &self.definitions, self.options)
232                    .map(Ok)
233                {
234                    return Some(event);
235                }
236                self.phase = Phase::Body;
237                self.closing = true;
238            }
239            Phase::Body => {}
240            Phase::Separator(end) => {
241                self.phase = Phase::Body;
242                return Some(Ok(self.separator(end)));
243            }
244            _ => return self.document_boundary(),
245        }
246        if self.cursor == 0 {
247            return self.document_boundary();
248        }
249        let index = self.cursor;
250        let node = self.blocks.nodes[index];
251        if self.closing {
252            let tag = match node.body {
253                Body::Paragraph => TagKind::Paragraph,
254                Body::Heading(_) => TagKind::Heading,
255                Body::Quote => TagKind::BlockQuote,
256                Body::Footnote(_) => TagKind::FootnoteDefinition,
257                Body::List { .. } => TagKind::List,
258                Body::Item(_) => TagKind::Item,
259                Body::Table => TagKind::Table,
260                Body::TableHead => TagKind::TableHead,
261                Body::TableBody => TagKind::TableBody,
262                Body::TableRow => TagKind::TableRow,
263                Body::TableCell { .. } => TagKind::TableCell,
264                _ => unreachable!("only block containers yield closing events"),
265            };
266            // Definitions are invisible; a following visible block terminates the output itself.
267            if !self.blocks.tight_paragraph(&node)
268                && !matches!(
269                    node.body,
270                    Body::Item(_) | Body::Footnote(_) | Body::TableCell { .. }
271                )
272                && (node.next != 0
273                    || node.body == Body::TableRow
274                        && self.blocks.nodes[node.parent].body == Body::TableHead
275                    || node.body == Body::Table
276                        && node.parent != 0
277                        && self.blocks.nodes[node.parent].end > node.end)
278                && !(matches!(node.body, Body::List { .. })
279                    && matches!(self.blocks.nodes[node.next].body, Body::Definition(_)))
280            {
281                self.phase = Phase::Separator(node.end);
282            }
283            self.advance();
284            let end = node.span_end(self.source);
285            return Some(Ok(Event {
286                kind: EventKind::End(tag),
287                span: end..end,
288            }));
289        }
290        let kind = match node.body {
291            Body::Paragraph | Body::Heading(_) | Body::TableCell { .. } => {
292                let nodes = &self.blocks.nodes;
293                let task = self.options.constructs.task_list_item
294                    && matches!(node.body, Body::Paragraph)
295                    && matches!(nodes[node.parent].body, Body::Item(_))
296                    && self.source[node.start..node.end].starts_with('[')
297                    && self
298                        .blocks
299                        .children(node.parent)
300                        .take_while(|&(before, _)| before != index)
301                        .all(|(_, child)| matches!(child.body, Body::Definition(_)));
302                let table = matches!(node.body, Body::TableCell { .. });
303                if self.extensions {
304                    self.prepare_inline::<true>(index, task, table);
305                } else {
306                    self.prepare_inline::<false>(index, task, table);
307                }
308                self.phase = Phase::Inline;
309                match node.body {
310                    Body::Heading(level) => EventKind::Start(Tag::Heading(level)),
311                    Body::Paragraph => EventKind::Start(Tag::Paragraph {
312                        tight: self.blocks.tight_paragraph(&node),
313                    }),
314                    Body::TableCell { alignment, header } => {
315                        EventKind::Start(Tag::TableCell { alignment, header })
316                    }
317                    _ => unreachable!("this branch handles inline block containers"),
318                }
319            }
320            Body::Quote => EventKind::Start(Tag::BlockQuote),
321            Body::Footnote(index) => EventKind::Start(Tag::FootnoteDefinition(Text::from(
322                self.definitions.footnote_key(index).to_string(),
323            ))),
324            Body::Opaque => {
325                self.advance();
326                EventKind::Opaque {
327                    value: Text::Source(node.start..node.end),
328                    block: true,
329                }
330            }
331            Body::Table => EventKind::Start(Tag::Table),
332            Body::TableHead => EventKind::Start(Tag::TableHead),
333            Body::TableBody => EventKind::Start(Tag::TableBody),
334            Body::TableRow => EventKind::Start(Tag::TableRow),
335            Body::List {
336                tight,
337                marker,
338                start,
339            } => EventKind::Start(Tag::List {
340                tight,
341                start: matches!(marker, b'.' | b')').then_some(start),
342            }),
343            Body::Item(_) => {
344                let block_end = self
345                    .blocks
346                    .children(index)
347                    .filter(|(_, child)| {
348                        !matches!(child.body, Body::Definition(_) | Body::Footnote(_))
349                    })
350                    .last()
351                    .is_some_and(|(_, child)| !self.blocks.tight_paragraph(child));
352                EventKind::Start(Tag::Item { block_end })
353            }
354            Body::Rule => {
355                if node.next != 0 {
356                    self.phase = Phase::Separator(node.end);
357                }
358                self.advance();
359                EventKind::Rule
360            }
361            Body::Definition(index) => {
362                let definition = &self.definitions.entries[index];
363                let kind = EventKind::Definition {
364                    label: definition.label.clone(),
365                    destination: definition.destination.clone(),
366                    title: definition.title.clone(),
367                };
368                self.advance();
369                let end = terminal_ending_start(self.source.as_bytes(), node.end);
370                return Some(Ok(Event {
371                    kind,
372                    span: node.start..end,
373                }));
374            }
375            Body::Html(end) => {
376                let kind = EventKind::Html {
377                    value: self.value(index),
378                    block: true,
379                };
380                if node.next != 0 && end < node.end {
381                    self.phase = Phase::Separator(node.end);
382                }
383                self.advance();
384                return Some(Ok(Event {
385                    kind,
386                    span: node.start..end,
387                }));
388            }
389            Body::Code(metadata) => {
390                let code = &self.blocks.codes[metadata];
391                let value = self.value(index);
392                let kind = EventKind::CodeBlock {
393                    value,
394                    language: if code.info.is_empty() {
395                        None
396                    } else {
397                        Some(crate::value_builder::decode_string_with_options(
398                            self.source,
399                            code.info.clone(),
400                            self.options,
401                        ))
402                    },
403                    fence: code.fence,
404                    math: code.math,
405                    unclosed: code.unclosed,
406                };
407                if node.next != 0 && !code.unclosed {
408                    self.phase = Phase::Separator(node.end);
409                }
410                self.advance();
411                kind
412            }
413            _ => unreachable!("root and source fragments are not semantic blocks"),
414        };
415        if matches!(
416            node.body,
417            Body::Quote
418                | Body::Footnote(_)
419                | Body::List { .. }
420                | Body::Item(_)
421                | Body::Table
422                | Body::TableHead
423                | Body::TableBody
424                | Body::TableRow
425        ) {
426            if node.child == 0 {
427                self.closing = true;
428            } else {
429                self.cursor = node.child;
430            }
431        }
432        Some(Ok(Event {
433            kind,
434            span: node.start..node.span_end(self.source),
435        }))
436    }
437}