Skip to main content

base_markdown/
stream.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! The shared pull-based semantic event contract.
5use crate::{CodeFence, LineEnding};
6use alloc::{borrow::Cow, string::String};
7use core::{fmt, ops::Range};
8
9/// A semantic value, independent of the lifetime of parser scratch storage.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum Text<'a> {
12    /// Unchanged UTF-8 bytes in the document's immutable source buffer.
13    Source(Range<usize>),
14    /// A decoded constant requiring no owned allocation.
15    Literal(&'static str),
16    /// A constructed or edited value.
17    Value(Cow<'a, str>),
18}
19
20#[cfg(feature = "serde")]
21impl serde::Serialize for Text<'_> {
22    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
23        match self {
24            Self::Source(range) => serializer.serialize_newtype_variant("Text", 0, "Source", range),
25            Self::Literal(value) => serializer.serialize_newtype_variant("Text", 1, "Value", value),
26            Self::Value(value) => serializer.serialize_newtype_variant("Text", 1, "Value", value),
27        }
28    }
29}
30#[cfg(feature = "serde")]
31impl<'de> serde::Deserialize<'de> for Text<'_> {
32    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
33        #[derive(serde::Deserialize)]
34        enum Stored {
35            Source(Range<usize>),
36            Value(String),
37            Literal(String),
38        }
39        Ok(match Stored::deserialize(deserializer)? {
40            Stored::Source(range) => Self::Source(range),
41            Stored::Value(value) | Stored::Literal(value) => Self::Value(Cow::Owned(value)),
42        })
43    }
44}
45
46impl Text<'_> {
47    /// Resolve this value against its source document.
48    ///
49    /// # Errors
50    /// Returns [`StreamError::InvalidRange`] for invalid UTF-8 boundaries or out-of-bounds ranges.
51    pub fn resolve<'a>(&'a self, source: &'a str) -> Result<&'a str, StreamError> {
52        match self {
53            Self::Source(range) => source.get(range.clone()).ok_or(StreamError::InvalidRange),
54            Self::Literal(value) => Ok(value),
55            Self::Value(value) => Ok(value),
56        }
57    }
58
59    pub(crate) fn borrowed(&self) -> Text<'_> {
60        match self {
61            Self::Source(range) => Text::Source(range.clone()),
62            Self::Literal(value) => Text::Literal(value),
63            Self::Value(value) => Text::Value(Cow::Borrowed(value)),
64        }
65    }
66
67    pub(crate) fn to_mut(&mut self, source: &str) -> Result<&mut String, StreamError> {
68        if !matches!(self, Self::Value(_)) {
69            *self = Self::Value(Cow::Owned(self.resolve(source)?.into()));
70        }
71        match self {
72            Self::Value(value) => Ok(value.to_mut()),
73            _ => unreachable!("source and literal text were materialized above"),
74        }
75    }
76
77    pub(crate) fn materialized(self, source: &str) -> Text<'static> {
78        match self {
79            Self::Source(range) => Text::from(String::from(
80                source.get(range).expect("document ranges are validated"),
81            )),
82            Self::Literal(value) => Text::Literal(value),
83            Self::Value(value) => Text::Value(Cow::Owned(value.into_owned())),
84        }
85    }
86
87    pub(crate) fn owned(self) -> Text<'static> {
88        match self {
89            Self::Source(range) => Text::Source(range),
90            Self::Literal(value) => Text::Literal(value),
91            Self::Value(value) => Text::Value(Cow::Owned(value.into_owned())),
92        }
93    }
94}
95
96impl From<String> for Text<'static> {
97    fn from(value: String) -> Self {
98        Self::Value(Cow::Owned(value))
99    }
100}
101
102/// The identity of a semantic container, used to balance start/end events.
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105pub enum TagKind {
106    /// The document root.
107    Document,
108    /// A paragraph.
109    Paragraph,
110    /// A heading.
111    Heading,
112    /// A block quote.
113    BlockQuote,
114    /// An ordered or unordered list.
115    List,
116    /// A list item.
117    Item,
118    /// Emphasized inline content.
119    Emphasis,
120    /// Strong inline content.
121    Strong,
122    /// Deleted inline content.
123    Delete,
124    /// A link with a resolved destination.
125    Link,
126    /// An image with inline alternative text.
127    Image,
128    /// A table.
129    Table,
130    /// A table head.
131    TableHead,
132    /// A table body.
133    TableBody,
134    /// A table row.
135    TableRow,
136    /// A table cell.
137    TableCell,
138    /// A footnote definition.
139    FootnoteDefinition,
140}
141
142impl TagKind {
143    // Capture containers cannot occur inside heading capture or image alt text.
144    // Ordinary inline nesting remains unrestricted by the rendering contract.
145    pub(crate) fn allows_descendant(self, child: Self) -> bool {
146        !matches!(self, Self::Heading | Self::Image)
147            || !matches!(child, Self::Heading | Self::FootnoteDefinition)
148    }
149}
150
151/// A container whose metadata is resolved before its start is yielded.
152#[derive(Clone, Debug, PartialEq, Eq)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154pub enum Tag<'a> {
155    /// The preferred output line ending, inferred from parsed block structure.
156    Document(Option<LineEnding>),
157    /// Whether the containing list suppresses paragraph tags.
158    Paragraph { tight: bool },
159    /// A heading level from one through six.
160    Heading(u8),
161    /// A block quote.
162    BlockQuote,
163    /// A list, its starting number (if ordered), and tightness.
164    List { start: Option<u32>, tight: bool },
165    /// A list item; whether its closing tag needs a preceding line break.
166    Item { block_end: bool },
167    /// Emphasized inline content.
168    Emphasis,
169    /// Strong inline content.
170    Strong,
171    /// Deleted inline content.
172    Delete,
173    /// Resolved link destination and optional title.
174    Link {
175        destination: Text<'a>,
176        title: Option<Text<'a>>,
177    },
178    /// Resolved image destination and optional title.
179    Image {
180        destination: Text<'a>,
181        title: Option<Text<'a>>,
182    },
183    /// A table.
184    Table,
185    /// A table head.
186    TableHead,
187    /// A table body.
188    TableBody,
189    /// A table row.
190    TableRow,
191    /// A cell with its own resolved alignment and header status.
192    TableCell {
193        alignment: crate::AlignKind,
194        header: bool,
195    },
196    /// A footnote definition, identified by its normalized label.
197    FootnoteDefinition(Text<'a>),
198}
199
200impl<'s> Tag<'s> {
201    /// Identify the matching end event.
202    pub fn kind(&self) -> TagKind {
203        match self {
204            Self::Document(_) => TagKind::Document,
205            Self::Paragraph { .. } => TagKind::Paragraph,
206            Self::Heading(_) => TagKind::Heading,
207            Self::BlockQuote => TagKind::BlockQuote,
208            Self::List { .. } => TagKind::List,
209            Self::Item { .. } => TagKind::Item,
210            Self::Emphasis => TagKind::Emphasis,
211            Self::Strong => TagKind::Strong,
212            Self::Delete => TagKind::Delete,
213            Self::Link { .. } => TagKind::Link,
214            Self::Image { .. } => TagKind::Image,
215            Self::Table => TagKind::Table,
216            Self::TableHead => TagKind::TableHead,
217            Self::TableBody => TagKind::TableBody,
218            Self::TableRow => TagKind::TableRow,
219            Self::TableCell { .. } => TagKind::TableCell,
220            Self::FootnoteDefinition(_) => TagKind::FootnoteDefinition,
221        }
222    }
223
224    pub(crate) fn map_text<'a>(self, mut map: impl FnMut(Text<'s>) -> Text<'a>) -> Tag<'a> {
225        match self {
226            Self::Document(value) => Tag::Document(value),
227            Self::Paragraph { tight } => Tag::Paragraph { tight },
228            Self::Heading(value) => Tag::Heading(value),
229            Self::BlockQuote => Tag::BlockQuote,
230            Self::List { start, tight } => Tag::List { start, tight },
231            Self::Item { block_end } => Tag::Item { block_end },
232            Self::Emphasis => Tag::Emphasis,
233            Self::Strong => Tag::Strong,
234            Self::Delete => Tag::Delete,
235            Self::Link { destination, title } => Tag::Link {
236                destination: map(destination),
237                title: title.map(&mut map),
238            },
239            Self::Image { destination, title } => Tag::Image {
240                destination: map(destination),
241                title: title.map(&mut map),
242            },
243            Self::Table => Tag::Table,
244            Self::TableHead => Tag::TableHead,
245            Self::TableBody => Tag::TableBody,
246            Self::TableRow => Tag::TableRow,
247            Self::TableCell { alignment, header } => Tag::TableCell { alignment, header },
248            Self::FootnoteDefinition(value) => Tag::FootnoteDefinition(map(value)),
249        }
250    }
251}
252
253/// A resolved leaf, or a start/end of a semantic container.
254#[derive(Clone, Debug, PartialEq, Eq)]
255#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
256pub enum EventKind<'a> {
257    /// Open a container.
258    Start(Tag<'a>),
259    /// Close the matching container.
260    End(TagKind),
261    /// Ordinary decoded text.
262    Text(Text<'a>),
263    /// A soft source line break within inline content.
264    SoftBreak(Text<'a>),
265    /// A block separator; conditional separators only terminate nonempty output.
266    Separator { value: Text<'a>, conditional: bool },
267    /// An explicit hard break.
268    HardBreak,
269    /// A thematic break.
270    Rule,
271    /// An inline code span; optional language metadata is an extension.
272    Code {
273        value: Text<'a>,
274        language: Option<Text<'a>>,
275    },
276    /// A code/math block, including source fence provenance where available.
277    CodeBlock {
278        value: Text<'a>,
279        language: Option<Text<'a>>,
280        fence: Option<CodeFence>,
281        math: bool,
282        unclosed: bool,
283    },
284    /// Inline mathematics.
285    Math(Text<'a>),
286    /// Raw HTML.
287    Html { value: Text<'a>, block: bool },
288    /// A task-list marker.
289    TaskMarker(bool),
290    /// A resolved definition, retained in the AST but omitted from HTML.
291    Definition {
292        label: Text<'a>,
293        destination: Text<'a>,
294        title: Option<Text<'a>>,
295    },
296    /// Frontmatter retained verbatim and omitted from HTML.
297    Opaque { value: Text<'a>, block: bool },
298    /// A call to a footnote definition.
299    FootnoteReference(Text<'a>),
300}
301
302impl<'s> EventKind<'s> {
303    pub(crate) fn validate(&self, source: Option<&str>) -> Result<(), StreamError> {
304        let text = |value: &Text<'_>| {
305            if let Some(source) = source {
306                value.resolve(source)?;
307            }
308            Ok(())
309        };
310        let optional = |value: &Option<Text<'_>>| {
311            if let Some(value) = value {
312                text(value)?;
313            }
314            Ok(())
315        };
316        match self {
317            Self::Start(Tag::Heading(level)) if !(1..=6).contains(level) => {
318                Err(StreamError::Unbalanced)
319            }
320            Self::Start(Tag::Link { destination, title } | Tag::Image { destination, title }) => {
321                text(destination)?;
322                optional(title)
323            }
324            Self::Start(Tag::FootnoteDefinition(value))
325            | Self::Text(value)
326            | Self::SoftBreak(value)
327            | Self::Math(value)
328            | Self::Separator { value, .. }
329            | Self::Html { value, .. }
330            | Self::Opaque { value, .. }
331            | Self::FootnoteReference(value) => text(value),
332            Self::Code { value, language }
333            | Self::CodeBlock {
334                value, language, ..
335            } => {
336                text(value)?;
337                optional(language)
338            }
339            Self::Definition {
340                label,
341                destination,
342                title,
343            } => {
344                text(label)?;
345                text(destination)?;
346                optional(title)
347            }
348            _ => Ok(()),
349        }
350    }
351
352    pub(crate) fn map_text<'a>(self, mut map: impl FnMut(Text<'s>) -> Text<'a>) -> EventKind<'a> {
353        match self {
354            Self::Start(tag) => EventKind::Start(tag.map_text(map)),
355            Self::End(kind) => EventKind::End(kind),
356            Self::Text(text) => EventKind::Text(map(text)),
357            Self::SoftBreak(text) => EventKind::SoftBreak(map(text)),
358            Self::Separator { value, conditional } => EventKind::Separator {
359                value: map(value),
360                conditional,
361            },
362            Self::HardBreak => EventKind::HardBreak,
363            Self::Rule => EventKind::Rule,
364            Self::Code { value, language } => EventKind::Code {
365                value: map(value),
366                language: language.map(map),
367            },
368            Self::CodeBlock {
369                value,
370                language,
371                fence,
372                math,
373                unclosed,
374            } => EventKind::CodeBlock {
375                value: map(value),
376                language: language.map(map),
377                fence,
378                math,
379                unclosed,
380            },
381            Self::Math(value) => EventKind::Math(map(value)),
382            Self::Html { value, block } => EventKind::Html {
383                value: map(value),
384                block,
385            },
386            Self::TaskMarker(value) => EventKind::TaskMarker(value),
387            Self::Definition {
388                label,
389                destination,
390                title,
391            } => EventKind::Definition {
392                label: map(label),
393                destination: map(destination),
394                title: title.map(map),
395            },
396            Self::Opaque { value, block } => EventKind::Opaque {
397                value: map(value),
398                block,
399            },
400            Self::FootnoteReference(value) => EventKind::FootnoteReference(map(value)),
401        }
402    }
403}
404
405/// A semantic event and its original source extent.
406#[derive(Clone, Debug, PartialEq, Eq)]
407#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
408pub struct Event<'a> {
409    /// Resolved semantic meaning.
410    pub kind: EventKind<'a>,
411    /// Original byte extent, independent of transformed or edited text.
412    pub span: Range<usize>,
413}
414
415impl Event<'_> {
416    pub(crate) fn validate(&self, source: Option<&str>) -> Result<(), StreamError> {
417        if self.span.start > self.span.end
418            || (matches!(self.kind, EventKind::End(_)) && !self.span.is_empty())
419            || source.is_some_and(|source| source.get(self.span.clone()).is_none())
420        {
421            return Err(StreamError::InvalidRange);
422        }
423        self.kind.validate(source)
424    }
425}
426
427/// An error while parsing, consuming, or writing a semantic stream.
428#[derive(Debug)]
429pub enum StreamError {
430    /// A custom event producer reported a diagnostic.
431    Parse(crate::message::Message),
432    /// A source range is out of bounds or splits a UTF-8 character.
433    InvalidRange,
434    /// Start and end events do not form a balanced document.
435    Unbalanced,
436    /// The caller's output writer failed.
437    Write(fmt::Error),
438}
439
440impl fmt::Display for StreamError {
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        match self {
443            Self::Parse(error) => write!(f, "{error}"),
444            Self::InvalidRange => f.write_str("invalid source range"),
445            Self::Unbalanced => f.write_str("unbalanced semantic event stream"),
446            Self::Write(error) => write!(f, "{error}"),
447        }
448    }
449}
450
451impl From<fmt::Error> for StreamError {
452    fn from(error: fmt::Error) -> Self {
453        Self::Write(error)
454    }
455}
456
457impl From<crate::message::Message> for StreamError {
458    fn from(error: crate::message::Message) -> Self {
459        Self::Parse(error)
460    }
461}