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}