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 (``) 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}