Skip to main content

docspec_core/
event.rs

1//! Event types for the streaming document pipeline.
2//!
3//! `DocSpec` documents are streams of typed events. Readers ([`crate::EventSource`])
4//! emit events; writers ([`crate::EventSink`]) consume them in document order.
5//! This module defines every event type and the rules for well-formed streams.
6//!
7//! For higher-level design decisions, see the
8//! [Architecture document](https://github.com/docspec/docspec/blob/main/ARCHITECTURE.md).
9//!
10//! # Error Handling
11//!
12//! Events never carry errors; errors flow out-of-band via [`crate::Result`]. See
13//! [`crate::EventSource::next_event`] for full semantics. Readers recover silently
14//! when possible (missing optional attributes, unrecognized elements, unsupported
15//! features) and return `Err` only on fatal conditions (malformed structure,
16//! truncated stream, invalid encoding).
17//!
18//! # Asset References
19//!
20//! [`Event::Image`] carries an [`crate::ImageSource`] (asset id or URI), not bytes.
21//! Writers resolve bytes lazily via [`crate::AssetProvider`]; assets must remain
22//! accessible until [`Event::EndDocument`].
23//!
24//! # Well-Formedness Rules
25//!
26//! Readers MUST produce well-formed streams; writers MAY assume well-formedness.
27//!
28//! 1. Every `Start*` has exactly one matching `End*`. They nest but never overlap.
29//! 2. Exactly one root: [`Event::StartDocument`]. Empty containers are valid.
30//! 3. [`Event::Text`] appears only inside containers, never at root.
31//! 4. [`Event::StartLink`] appears inside inline-accepting blocks (paragraphs,
32//!    headings, list items, cells, definition details) and does not nest.
33//! 5. List items ([`Event::StartOrderedListItem`], [`Event::StartUnorderedListItem`])
34//!    appear inside block containers and may nest; `level` is 0-indexed.
35//! 6. [`Event::StartCaption`] appears at most once per table, before any rows.
36//! 7. Each footnote id appears in exactly one [`Event::FootnoteRef`] and one
37//!    [`Event::StartFootnote`].
38//! 8. [`Event::StartTableRow`] appears only inside [`Event::StartTable`];
39//!    [`Event::StartTableCell`] and [`Event::StartTableHeader`] only inside
40//!    [`Event::StartTableRow`].
41//! 9. Readers MUST normalize overlapping source styles into nested
42//!    [`Event::StartTextStyle`] spans via close-and-reopen.
43//! 10. All open [`Event::StartTextStyle`] spans MUST close before the enclosing
44//!     block-end event ([`Event::EndParagraph`], [`Event::EndHeading`],
45//!     [`Event::EndOrderedListItem`], [`Event::EndUnorderedListItem`],
46//!     [`Event::EndTableCell`], [`Event::EndTableHeader`], [`Event::EndCaption`],
47//!     [`Event::EndDefinitionTerm`], [`Event::EndDefinitionDetail`]).
48//! 11. [`Event::StartTextStyle`] and [`Event::StartPreformatted`] MUST NOT nest
49//!     inside each other.
50//! 12. Inside a link, [`Event::StartLink`] SHOULD be the outer container and
51//!     [`Event::StartTextStyle`] the inner.
52//! 13. Empty [`Event::StartTextStyle`] spans MUST NOT be emitted: at least one
53//!     [`Event::Text`] event must appear before the matching
54//!     [`Event::EndTextStyle`].
55
56/// The kind of text formatting carried by a [`Event::StartTextStyle`] event.
57#[derive(Debug, Clone, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum TextStyleKind {
60    /// Bold formatting.
61    Bold,
62    /// Italic formatting.
63    Italic,
64    /// Monospace/code formatting.
65    Code,
66    /// Strikethrough formatting.
67    Strikethrough,
68    /// Underline formatting.
69    Underline,
70    /// Subscript formatting.
71    ///
72    /// May be active simultaneously with [`TextStyleKind::Superscript`] by nesting;
73    /// writers that cannot represent both prefer `Superscript`.
74    Subscript,
75    /// Superscript formatting.
76    ///
77    /// May be active simultaneously with [`TextStyleKind::Subscript`] by nesting;
78    /// writers that cannot represent both prefer `Superscript`.
79    Superscript,
80    /// Highlight/mark color formatting. The variant carries the highlight color.
81    Mark(crate::Color),
82    /// Foreground (text) color. Carries an explicit RGB color.
83    TextColor(crate::Color),
84}
85
86/// A streaming document event.
87///
88/// Events flow from [`crate::EventSource`] readers to [`crate::EventSink`] writers.
89/// The enum is marked `#[non_exhaustive]` to allow adding new event types in
90/// future versions; downstream consumers must include a wildcard `_ =>` arm when
91/// matching.
92///
93/// Events come in three categories:
94///
95/// - **Start/End pairs**: Container elements like headings, paragraphs, tables.
96///   Every `Start*` has exactly one matching `End*` (Rule 1 in module docs).
97/// - **Self-contained**: Standalone elements like text, images, line breaks.
98/// - **Block vs Inline**: Block events create new vertical sections; inline
99///   events flow within blocks.
100///
101/// See the [module-level documentation](self) for error handling, asset
102/// references, and the full well-formedness ruleset.
103#[non_exhaustive]
104#[derive(Debug, Clone, PartialEq)]
105pub enum Event {
106    /// End a block quote.
107    EndBlockQuote,
108
109    /// End a table caption.
110    EndCaption,
111
112    /// End a definition detail.
113    EndDefinitionDetail,
114
115    /// End a definition list.
116    EndDefinitionList,
117
118    /// End a definition term.
119    EndDefinitionTerm,
120
121    /// End a document.
122    EndDocument,
123
124    /// End a footnote definition.
125    EndFootnote,
126
127    /// End a heading.
128    EndHeading,
129
130    /// End a hyperlink.
131    EndLink,
132
133    /// End an ordered (numbered) list item.
134    EndOrderedListItem,
135
136    /// End a paragraph.
137    EndParagraph,
138
139    /// End a preformatted block.
140    EndPreformatted,
141
142    /// End a table.
143    EndTable,
144
145    /// End a table data cell.
146    EndTableCell,
147
148    /// End a table header cell.
149    EndTableHeader,
150
151    /// End a table row.
152    EndTableRow,
153
154    /// End an inline text style span.
155    EndTextStyle,
156
157    /// End an unordered (bulleted) list item.
158    EndUnorderedListItem,
159
160    /// A reference to a footnote.
161    ///
162    /// Inline marker; the corresponding [`Event::StartFootnote`] definition appears
163    /// elsewhere in the stream (before or after this reference, depending on source
164    /// format). Each footnote ID appears in exactly one `FootnoteRef` and one
165    /// [`Event::StartFootnote`] (Rule 7 in module docs).
166    FootnoteRef {
167        /// The footnote identifier being referenced.
168        id: u32,
169    },
170
171    /// An image reference.
172    ///
173    /// Asset bytes resolve lazily via [`crate::AssetProvider`]. `decorative` means
174    /// purely visual — no alt text is needed for accessibility. Images may appear
175    /// inline within paragraphs/headings or directly in block containers.
176    Image {
177        /// Alternative text for accessibility.
178        alt: Option<String>,
179        /// Whether the image is purely decorative (no alt text needed).
180        decorative: bool,
181        /// Optional block identifier for the image.
182        id: Option<String>,
183        /// Source of the image (embedded asset or external URI).
184        source: crate::ImageSource,
185        /// Optional tooltip text.
186        title: Option<String>,
187    },
188
189    /// A hard line break within a paragraph.
190    ///
191    /// Explicit hard break (e.g., markdown two-space-newline, HTML `<br>`).
192    LineBreak,
193
194    /// A soft line break in source markup, such as a markdown line wrap.
195    ///
196    /// Soft breaks correspond to source line wraps that do not enforce a
197    /// visible break. Writers choose rendering policy: space, newline,
198    /// `<br>`, etc.
199    SoftBreak,
200
201    /// Begin a block quote.
202    ///
203    /// May contain any block element.
204    StartBlockQuote {
205        /// Optional block identifier.
206        id: Option<String>,
207    },
208
209    /// Begin a table caption.
210    ///
211    /// Appears at most once per table, before any rows (Rule 6 in module docs).
212    StartCaption {
213        /// Optional block identifier.
214        id: Option<String>,
215    },
216
217    /// Begin a definition detail (description).
218    ///
219    /// Details can contain any block element.
220    StartDefinitionDetail {
221        /// Optional block identifier.
222        id: Option<String>,
223    },
224
225    /// Begin a definition list.
226    ///
227    /// Contains [`Event::StartDefinitionTerm`] / [`Event::StartDefinitionDetail`]
228    /// pairs.
229    StartDefinitionList {
230        /// Optional block identifier.
231        id: Option<String>,
232    },
233
234    /// Begin a definition term.
235    ///
236    /// Terms contain inline content only.
237    StartDefinitionTerm {
238        /// Optional block identifier.
239        id: Option<String>,
240    },
241
242    /// Begin a document with optional language and metadata.
243    ///
244    /// The root container — exactly one per stream (Rule 2 in module docs).
245    /// `language` is a BCP 47 tag (e.g., `"en"`, `"en-US"`, `"zh-Hans"`).
246    StartDocument {
247        /// Optional block identifier.
248        id: Option<String>,
249        /// BCP 47 language tag (e.g., "en", "en-US", "zh-Hans").
250        language: Option<String>,
251        /// Document metadata including title, authors, and description.
252        metadata: Option<crate::DocumentMeta>,
253    },
254
255    /// Begin a footnote definition.
256    ///
257    /// Readers emit `StartFootnote` as soon as practical; placement varies by
258    /// source format. The corresponding [`Event::FootnoteRef`] may appear before
259    /// or after this definition. Writers decide final placement and must buffer
260    /// if needed. Footnotes contain paragraphs only; this restriction may relax
261    /// in future versions.
262    StartFootnote {
263        /// Unique identifier for this footnote.
264        id: u32,
265    },
266
267    /// Begin a heading of the given level.
268    ///
269    /// Levels 1–6 are standard (HTML). DOCX/ODT/RTF support 1–9. Writers clamp
270    /// higher levels to their format's maximum. Heading levels are 1-based (range
271    /// 1–9); list item `level` (on [`Event::StartOrderedListItem`] and
272    /// [`Event::StartUnorderedListItem`]) is 0-indexed.
273    StartHeading {
274        /// Optional block identifier for the heading.
275        id: Option<String>,
276        /// Heading level, 1–9 (1 is most prominent).
277        level: u8,
278    },
279
280    /// Begin a hyperlink.
281    ///
282    /// An inline container (uses Start/End because it carries `href`). Valid
283    /// inside paragraphs, headings, list items, cells, and definition details.
284    /// Links do not nest (Rule 4 in module docs).
285    StartLink {
286        /// URL or URI target of the link.
287        href: String,
288        /// Optional block identifier.
289        id: Option<String>,
290        /// Optional tooltip text.
291        title: Option<String>,
292    },
293
294    /// Begin an ordered (numbered) list item.
295    ///
296    /// See [`Event::StartUnorderedListItem`] for nesting and list-boundary
297    /// semantics — they apply identically here. `start` is populated only on
298    /// the first item of an ordered list; subsequent items use `None`.
299    StartOrderedListItem {
300        /// Optional block identifier.
301        id: Option<String>,
302        /// Zero-indexed nesting depth (0 = top-level list).
303        level: u32,
304        /// Starting number for the list, populated only on the first item of an ordered list
305        /// (subsequent items in the same list: `None`).
306        start: Option<u64>,
307        /// Visual style of the list marker. Writers tolerate mismatches per `ListStyleType` convention.
308        style_type: crate::ListStyleType,
309    },
310
311    /// Begin a paragraph with optional alignment.
312    StartParagraph {
313        /// Text alignment for the paragraph.
314        alignment: Option<crate::TextAlignment>,
315        /// Optional block identifier for the paragraph.
316        id: Option<String>,
317    },
318
319    /// Begin a preformatted (code) block with optional syntax highlighting.
320    ///
321    /// Inside `StartPreformatted`/[`Event::EndPreformatted`], no
322    /// [`Event::StartTextStyle`] events appear (Rule 11 in module docs). When
323    /// `syntax` is present, the block has code semantics. Newlines in content
324    /// are literal.
325    StartPreformatted {
326        /// Optional block identifier.
327        id: Option<String>,
328        /// Language identifier for syntax highlighting (e.g., "rust", "python").
329        syntax: Option<String>,
330    },
331
332    /// Begin a table.
333    ///
334    /// Contains an optional [`Event::StartCaption`] (at most one, before any
335    /// rows), then [`Event::StartTableRow`] events. Cells may contain any block
336    /// element.
337    StartTable {
338        /// Optional block identifier.
339        id: Option<String>,
340    },
341
342    /// Begin a table data cell.
343    ///
344    /// Data cells omit the `scope` and `abbr` fields carried by
345    /// [`Event::StartTableHeader`]; use the header variant for cells that
346    /// describe other cells.
347    StartTableCell {
348        /// Number of columns this cell spans.
349        colspan: Option<u32>,
350        /// Optional block identifier.
351        id: Option<String>,
352        /// Number of rows this cell spans.
353        rowspan: Option<u32>,
354    },
355
356    /// Begin a table header cell.
357    ///
358    /// Header cells carry `scope` and `abbr` for accessibility; data cells (use
359    /// [`Event::StartTableCell`]) omit these.
360    StartTableHeader {
361        /// Abbreviated content for accessibility.
362        abbr: Option<String>,
363        /// Number of columns this cell spans.
364        colspan: Option<u32>,
365        /// Optional block identifier.
366        id: Option<String>,
367        /// Number of rows this cell spans.
368        rowspan: Option<u32>,
369        /// Whether this header applies to a column or row.
370        scope: Option<crate::TableHeaderScope>,
371    },
372
373    /// Begin a table row.
374    StartTableRow {
375        /// Optional block identifier.
376        id: Option<String>,
377    },
378
379    /// Begin an inline text style span.
380    ///
381    /// Valid inside paragraphs, headings, list items, cells, and definition
382    /// details. Style spans nest but never overlap (Rules 1 and 9 in module
383    /// docs); readers MUST close-and-reopen to express overlapping source
384    /// styles. The [`TextStyleKind::Mark`] variant carries the highlight color;
385    /// [`TextStyleKind::TextColor`] carries the foreground text color.
386    StartTextStyle {
387        /// The style kind opened by this span.
388        kind: TextStyleKind,
389        /// Optional block identifier for the style span.
390        id: Option<String>,
391    },
392
393    /// Begin an unordered (bulleted) list item.
394    ///
395    /// Child items nest inside the parent's `Start*`/`End*` pair; the parent's
396    /// [`Event::EndUnorderedListItem`] appears AFTER all children and any
397    /// continuation content (paragraphs, line breaks) belonging to the parent.
398    /// `level` is 0-indexed and authoritative — writers may rely on it alone.
399    ///
400    /// **List boundaries** (applies to [`Event::StartOrderedListItem`] as well):
401    /// a new list begins when (a) a non-list block intervenes, (b) ordered vs.
402    /// unordered changes at the same level, or (c) level decreases then
403    /// increases without a parent.
404    StartUnorderedListItem {
405        /// Optional block identifier.
406        id: Option<String>,
407        /// Zero-indexed nesting depth (0 = top-level list).
408        level: u32,
409        /// Visual style of the list marker. Writers tolerate mismatches per `ListStyleType` convention.
410        style_type: crate::ListStyleType,
411    },
412
413    /// A text run.
414    ///
415    /// Whitespace is significant. Outside preformatted blocks, newlines in
416    /// content are collapsed to whitespace; readers emit [`Event::LineBreak`]
417    /// for explicit hard breaks (e.g., markdown two-space-newline, HTML `<br>`)
418    /// and [`Event::SoftBreak`] for soft breaks (e.g., source line wraps
419    /// within a paragraph). Inline formatting is expressed via surrounding
420    /// [`Event::StartTextStyle`]/[`Event::EndTextStyle`] wrapper events; the
421    /// `Text` event itself carries content only.
422    Text {
423        /// The text content.
424        content: String,
425    },
426
427    /// A horizontal rule / thematic break.
428    ///
429    /// Section separator. Self-contained block event.
430    ThematicBreak {
431        /// Optional block identifier.
432        id: Option<String>,
433    },
434}