Skip to main content

markdown_stream/
event.rs

1//! The parser's output: a flat, append-only stream of [`Event`]s (SAX-like, no AST).
2//!
3//! Faithful to the Go `stream/event.go` model: blocks are delimited by `EnterBlock`/`ExitBlock`
4//! pairs, inline content arrives as styled `Text` plus `SoftBreak`/`LineBreak`. A renderer consumes
5//! this stream directly and never re-parses Markdown.
6
7/// A byte range in the source, with the 1-based line/column of its start.
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9pub struct Span {
10    pub start: usize,
11    pub end: usize,
12    pub line: u32,
13    pub column: u32,
14}
15
16/// The kind of block an `EnterBlock`/`ExitBlock` event refers to.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum BlockKind {
19    Document,
20    Paragraph,
21    Heading,
22    BlockQuote,
23    List,
24    ListItem,
25    FencedCode,
26    IndentedCode,
27    ThematicBreak,
28    HtmlBlock,
29    Table,
30    TableRow,
31    TableCell,
32}
33
34/// Column alignment for GFM table columns.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Alignment {
37    None,
38    Left,
39    Center,
40    Right,
41}
42
43/// List metadata carried on an `EnterBlock(List)` event.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ListData {
46    /// `true` for ordered (`1.`) lists, `false` for bullet (`-`/`*`/`+`) lists.
47    pub ordered: bool,
48    /// Starting number for ordered lists.
49    pub start: u64,
50    /// `true` if the list is tight (no blank lines between items → no `<p>` wrappers).
51    pub tight: bool,
52    /// The marker character: `-`/`*`/`+` for bullets, `.`/`)` for ordered.
53    pub marker: char,
54}
55
56/// A link or image target carried on a styled `Text` event.
57#[derive(Debug, Clone, Default, PartialEq, Eq)]
58pub struct Link {
59    pub href: String,
60    pub title: String,
61    /// `true` if this is an image (`![alt](src)`) rather than a link.
62    pub image: bool,
63}
64
65/// A resolved link reference definition (`[label]: dest "title"`). The destination is already
66/// normalized (escapes/entities resolved, percent-encoded) and the title un-escaped, so resolving a
67/// reference link is just a map lookup. Parser-owned; not part of the public event stream.
68#[derive(Debug, Clone, Default, PartialEq, Eq)]
69pub struct LinkDef {
70    pub dest: String,
71    pub title: String,
72}
73
74/// The kind of inline span an `EnterInline`/`ExitInline` event opens or closes.
75///
76/// Unlike the cumulative [`InlineStyle`] flags carried on `Text` (which a flat renderer reads),
77/// these events make nesting *explicit*: an HTML renderer emits exactly one tag per enter/exit, so
78/// `*a **b** c*` nests as `<em>a <strong>b</strong> c</em>` rather than three independent runs.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum Inline {
81    Emphasis,
82    Strong,
83    Strikethrough,
84    Code,
85    Link(Link),
86    Image(Link),
87}
88
89/// Inline styling carried on a `Text` event. Multiple flags may apply at once
90/// (e.g. bold + italic). `link` is set for text inside a link/image.
91#[derive(Debug, Clone, Default, PartialEq, Eq)]
92pub struct InlineStyle {
93    pub emphasis: bool, // italic
94    pub strong: bool,   // bold
95    pub code: bool,     // inline code span
96    pub strikethrough: bool,
97    pub link: Option<Link>,
98    /// `true` for inline raw HTML (`<tag …>`, comments, …): the text is verbatim HTML and an HTML
99    /// renderer must emit it *unescaped*. The terminal renderer ignores this flag (the literal tag
100    /// text is shown as-is), so it has no effect on terminal output.
101    pub raw_html: bool,
102}
103
104impl InlineStyle {
105    /// `true` when no styling applies (plain text).
106    pub fn is_plain(&self) -> bool {
107        !self.emphasis
108            && !self.strong
109            && !self.code
110            && !self.strikethrough
111            && self.link.is_none()
112            && !self.raw_html
113    }
114}
115
116/// Variant-specific payload for an `EnterBlock` event. Defaults are empty/zero for blocks that
117/// carry no extra data (paragraphs, blockquotes, …).
118#[derive(Debug, Clone, Default, PartialEq, Eq)]
119pub struct BlockData {
120    /// Heading level (1–6) for `Heading`.
121    pub level: u8,
122    /// Fenced-code info string (the language) for `FencedCode`.
123    pub info: String,
124    /// List metadata for `List`.
125    pub list: Option<ListData>,
126    /// Per-column alignment for `Table`.
127    pub alignment: Vec<Alignment>,
128    /// For `HtmlBlock`: `true` when the block is a raw-text element (`<script>`/`<style>`/`<pre>`/
129    /// `<textarea>`), whose content the GFM tag filter leaves untouched. (Other HTML blocks — comments,
130    /// `<blockquote>`, … — are still filtered.)
131    pub html_raw_text: bool,
132}
133
134/// One parser event. The stream is a depth-first walk: every `EnterBlock` is eventually balanced by
135/// an `ExitBlock` of the same kind.
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum Event {
138    EnterBlock {
139        block: BlockKind,
140        data: BlockData,
141        span: Span,
142    },
143    ExitBlock {
144        block: BlockKind,
145        span: Span,
146    },
147    /// A run of inline text with its accumulated styling.
148    Text {
149        text: String,
150        style: InlineStyle,
151        span: Span,
152    },
153    /// Open an inline span (emphasis, strong, link, …). Balanced by a matching `ExitInline`.
154    EnterInline {
155        inline: Inline,
156        span: Span,
157    },
158    /// Close the most recently opened inline span of the matching `inline`.
159    ExitInline {
160        inline: Inline,
161    },
162    /// A newline within a paragraph (rendered as a space / `\n` in HTML).
163    SoftBreak,
164    /// A hard line break (two trailing spaces or a backslash).
165    LineBreak,
166}
167
168impl Event {
169    /// Convenience constructor for a plain (unstyled) text event.
170    pub fn text(s: impl Into<String>) -> Event {
171        Event::Text {
172            text: s.into(),
173            style: InlineStyle::default(),
174            span: Span::default(),
175        }
176    }
177
178    /// Convenience constructor for an `EnterBlock` with default data.
179    pub fn enter(block: BlockKind) -> Event {
180        Event::EnterBlock {
181            block,
182            data: BlockData::default(),
183            span: Span::default(),
184        }
185    }
186
187    /// Convenience constructor for an `ExitBlock`.
188    pub fn exit(block: BlockKind) -> Event {
189        Event::ExitBlock {
190            block,
191            span: Span::default(),
192        }
193    }
194}