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