Skip to main content

markdown_syntax/
ast.rs

1//! The owned Markdown AST that [`parse()`](crate::parse()) produces and
2//! `Document::to_markdown`/`to_html` consume. Every node carries a [`NodeMeta`]
3//! with an optional source [`Span`]. [`Block`] and [`Inline`] are the two node
4//! enums; everything else is a concrete node struct or a small enum describing
5//! a node's variant.
6
7use alloc::{string::String, vec::Vec};
8
9use crate::span::Span;
10
11/// Metadata attached to every AST node; currently just the source span.
12#[derive(Clone, Debug, Default, Eq, PartialEq)]
13pub struct NodeMeta {
14    /// The node's source location, or `None` for hand-built nodes.
15    pub span: Option<Span>,
16}
17
18impl NodeMeta {
19    /// Wrap an optional [`Span`] into a [`NodeMeta`].
20    pub const fn new(span: Option<Span>) -> Self {
21        Self { span }
22    }
23}
24
25/// The root of a parsed document: a sequence of top-level [`Block`]s.
26#[derive(Clone, Debug, Default, Eq, PartialEq)]
27pub struct Document {
28    /// Node metadata (source span).
29    pub meta: NodeMeta,
30    /// The document's top-level blocks, in source order.
31    pub children: Vec<Block>,
32}
33
34/// A block-level node: the building blocks of a document's vertical structure.
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub enum Block {
37    /// A paragraph of inline content.
38    Paragraph(Paragraph),
39    /// An ATX (`# h`) or setext (underlined) heading.
40    Heading(Heading),
41    /// A thematic break / horizontal rule: `---`, `***`, or `___`.
42    ThematicBreak(ThematicBreak),
43    /// A block quote: lines prefixed with `> `.
44    BlockQuote(BlockQuote),
45    /// A GFM alert / admonition: `> [!NOTE]` etc.
46    Alert(Alert),
47    /// A bullet or ordered list.
48    List(List),
49    /// A description / definition list (term + details).
50    DescriptionList(DescriptionList),
51    /// A fenced (```` ``` ````) or indented code block.
52    CodeBlock(CodeBlock),
53    /// A raw HTML block.
54    HtmlBlock(HtmlBlock),
55    /// A structured raw HTML container whose interior is parsed as Markdown.
56    HtmlContainer(HtmlContainer),
57    /// A link reference definition: `[label]: url "title"`.
58    Definition(Definition),
59    /// A footnote definition: `[^id]: text`.
60    FootnoteDefinition(FootnoteDefinition),
61    /// A GFM pipe table.
62    Table(Table),
63    /// A display math block: `$$ … $$`.
64    MathBlock(MathBlock),
65    /// A leading frontmatter block (`---` YAML or `+++` TOML).
66    Frontmatter(Frontmatter),
67    /// An MDX ESM block (`import`/`export` statements).
68    MdxEsm(MdxEsm),
69    /// A block-level MDX expression: `{ … }`.
70    MdxExpression(MdxExpression),
71    /// A block-level MDX JSX element.
72    MdxJsx(MdxJsx),
73    /// A leaf directive: `::name[label]{attrs}` (distinct from MDX).
74    LeafDirective(LeafDirective),
75    /// A container directive: `:::name … :::` (distinct from MDX).
76    ContainerDirective(ContainerDirective),
77}
78
79/// A paragraph: a run of inline content. Source: any plain text line(s).
80#[derive(Clone, Debug, Eq, PartialEq)]
81pub struct Paragraph {
82    /// Node metadata (source span).
83    pub meta: NodeMeta,
84    /// The paragraph's inline content.
85    pub children: Vec<Inline>,
86}
87
88/// A heading. Source: `# Title` (ATX) or `Title\n===` (setext).
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct Heading {
91    /// Node metadata (source span).
92    pub meta: NodeMeta,
93    /// Heading level, 1..=6.
94    pub depth: u8,
95    /// Whether the heading used ATX or setext syntax.
96    pub kind: HeadingKind,
97    /// The heading's inline content.
98    pub children: Vec<Inline>,
99}
100
101/// Which heading syntax produced a [`Heading`].
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum HeadingKind {
104    /// ATX heading: `# Title` … `###### Title`.
105    Atx,
106    /// Setext heading: `Title` underlined with `===` (level 1) or `---` (level 2).
107    Setext,
108}
109
110/// A thematic break / horizontal rule. Source: `---`, `***`, or `___`.
111#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct ThematicBreak {
113    /// Node metadata (source span).
114    pub meta: NodeMeta,
115    /// Which character formed the break.
116    pub marker: ThematicBreakMarker,
117}
118
119/// The character used to draw a [`ThematicBreak`].
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub enum ThematicBreakMarker {
122    /// Dashes: `---`.
123    Dash,
124    /// Asterisks: `***`.
125    Asterisk,
126    /// Underscores: `___`.
127    Underscore,
128}
129
130/// A block quote: content prefixed with `> `.
131#[derive(Clone, Debug, Eq, PartialEq)]
132pub struct BlockQuote {
133    /// Node metadata (source span).
134    pub meta: NodeMeta,
135    /// The quoted block content.
136    pub children: Vec<Block>,
137}
138
139/// A GFM alert / admonition. Source: `> [!NOTE]` followed by quoted content.
140#[derive(Clone, Debug, Eq, PartialEq)]
141pub struct Alert {
142    /// Node metadata (source span).
143    pub meta: NodeMeta,
144    /// The alert severity / type.
145    pub kind: AlertKind,
146    /// An optional custom title following the `[!KIND]` marker.
147    pub title: Option<String>,
148    /// The alert's block content.
149    pub children: Vec<Block>,
150}
151
152/// The kind of a GFM [`Alert`] (the `[!KIND]` marker).
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154pub enum AlertKind {
155    /// `> [!NOTE]`.
156    Note,
157    /// `> [!TIP]`.
158    Tip,
159    /// `> [!IMPORTANT]`.
160    Important,
161    /// `> [!WARNING]`.
162    Warning,
163    /// `> [!CAUTION]`.
164    Caution,
165}
166
167/// A bullet or ordered list. Source: `- a` / `1. a` lines.
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub struct List {
170    /// Node metadata (source span).
171    pub meta: NodeMeta,
172    /// `true` for an ordered list, `false` for a bullet list.
173    pub ordered: bool,
174    /// The starting number of an ordered list (e.g. `3.` => `Some(3)`).
175    pub start: Option<u64>,
176    /// The marker delimiter used by the list items.
177    pub delimiter: ListDelimiter,
178    /// `true` if the list is tight (no blank lines between items / no `<p>`).
179    pub tight: bool,
180    /// The list's items.
181    pub children: Vec<ListItem>,
182}
183
184/// The marker character that delimits a list's items.
185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
186pub enum ListDelimiter {
187    /// Bullet `-`.
188    Dash,
189    /// Bullet `*`.
190    Asterisk,
191    /// Bullet `+`.
192    Plus,
193    /// Ordered `1.`.
194    Period,
195    /// Ordered `1)`.
196    Paren,
197}
198
199/// A single list item, optionally a GFM task-list checkbox.
200#[derive(Clone, Debug, Eq, PartialEq)]
201pub struct ListItem {
202    /// Node metadata (source span).
203    pub meta: NodeMeta,
204    /// Task-list state: `Some(true)` for `[x]`, `Some(false)` for `[ ]`, `None` otherwise.
205    pub checked: Option<bool>,
206    /// The item's block content.
207    pub children: Vec<Block>,
208}
209
210/// A description / definition list of term + details pairs.
211#[derive(Clone, Debug, Eq, PartialEq)]
212pub struct DescriptionList {
213    /// Node metadata (source span).
214    pub meta: NodeMeta,
215    /// `true` if the list is tight (no blank lines between items).
216    pub tight: bool,
217    /// The list's term/details items.
218    pub children: Vec<DescriptionItem>,
219}
220
221/// One entry of a [`DescriptionList`]: a term and its detail blocks. Source: a
222/// term line followed by `: details` lines.
223#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct DescriptionItem {
225    /// Node metadata (source span).
226    pub meta: NodeMeta,
227    /// The term's inline content.
228    pub term: Vec<Inline>,
229    /// The detail group(s) attached to this term.
230    pub details: Vec<DescriptionDetails>,
231}
232
233/// The details (`: …`) attached to a [`DescriptionItem`]'s term.
234#[derive(Clone, Debug, Eq, PartialEq)]
235pub struct DescriptionDetails {
236    /// Node metadata (source span).
237    pub meta: NodeMeta,
238    /// The details' block content.
239    pub children: Vec<Block>,
240}
241
242/// A code block. Source: ```` ```lang … ``` ```` (fenced) or 4-space-indented lines.
243#[derive(Clone, Debug, Eq, PartialEq)]
244pub struct CodeBlock {
245    /// Node metadata (source span).
246    pub meta: NodeMeta,
247    /// Whether the block is fenced (and with what fence) or indented.
248    pub kind: CodeBlockKind,
249    /// The info string after a fence (e.g. the `rust` in ```` ```rust ````).
250    pub info: Option<String>,
251    /// The literal code contents.
252    pub value: String,
253}
254
255/// Whether a [`CodeBlock`] is fenced or indented.
256#[derive(Clone, Copy, Debug, Eq, PartialEq)]
257pub enum CodeBlockKind {
258    /// A fenced code block; records the fence char and its run length.
259    Fenced {
260        /// Which character formed the fence (backtick or tilde).
261        marker: FenceMarker,
262        /// The number of fence characters in the opening fence (>=3).
263        length: usize,
264    },
265    /// A 4-space-indented code block.
266    Indented,
267}
268
269/// The character used to fence a [`CodeBlock`].
270#[derive(Clone, Copy, Debug, Eq, PartialEq)]
271pub enum FenceMarker {
272    /// Backtick fence: ```` ``` ````.
273    Backtick,
274    /// Tilde fence: `~~~`.
275    Tilde,
276}
277
278/// A raw HTML block: HTML emitted verbatim.
279#[derive(Clone, Debug, Eq, PartialEq)]
280pub struct HtmlBlock {
281    /// Node metadata (source span).
282    pub meta: NodeMeta,
283    /// The literal HTML source.
284    pub value: String,
285}
286
287/// A structured raw HTML container, such as `<details> ... </details>`.
288#[derive(Clone, Debug, Eq, PartialEq)]
289pub struct HtmlContainer {
290    /// Node metadata (source span).
291    pub meta: NodeMeta,
292    /// The opening HTML tag.
293    pub opening: HtmlTag,
294    /// The parsed container content.
295    pub content: HtmlContainerContent,
296    /// The closing HTML tag.
297    pub closing: HtmlTag,
298}
299
300/// A raw HTML tag that bounds an [`HtmlContainer`].
301#[derive(Clone, Debug, Eq, PartialEq)]
302pub struct HtmlTag {
303    /// Node metadata (source span).
304    pub meta: NodeMeta,
305    /// The normalized lowercase tag name.
306    pub name: String,
307    /// The literal tag source, including angle brackets and attributes.
308    pub raw: String,
309}
310
311/// The parsed content inside an [`HtmlContainer`].
312#[derive(Clone, Debug, Eq, PartialEq)]
313pub enum HtmlContainerContent {
314    /// Block-level Markdown content.
315    Blocks(Vec<Block>),
316    /// Inline-level Markdown content.
317    Inlines(Vec<Inline>),
318}
319
320/// A link reference definition. Source: `[label]: destination "title"`.
321#[derive(Clone, Debug, Eq, PartialEq)]
322pub struct Definition {
323    /// Node metadata (source span).
324    pub meta: NodeMeta,
325    /// The label as written in the source (e.g. `Foo Bar`).
326    pub label: String,
327    /// The normalized lookup key (case-folded, whitespace-collapsed) for matching references.
328    pub identifier: String,
329    /// The link target URL.
330    pub destination: String,
331    /// How the destination was delimited (bare or `<…>`).
332    pub destination_kind: LinkDestinationKind,
333    /// The optional link title.
334    pub title: Option<String>,
335    /// How the title was quoted, if present.
336    pub title_kind: Option<LinkTitleKind>,
337}
338
339/// A footnote definition. Source: `[^id]: footnote text`.
340#[derive(Clone, Debug, Eq, PartialEq)]
341pub struct FootnoteDefinition {
342    /// Node metadata (source span).
343    pub meta: NodeMeta,
344    /// The label as written in the source (the text after `^`).
345    pub label: String,
346    /// The normalized lookup key matching [`FootnoteReference`]s.
347    pub identifier: String,
348    /// The footnote's block content.
349    pub children: Vec<Block>,
350}
351
352/// A GFM pipe table: a header row, an alignment row, then body rows. Source:
353/// `| a | b |` / `|---|---|` / `| 1 | 2 |`.
354#[derive(Clone, Debug, Eq, PartialEq)]
355pub struct Table {
356    /// Node metadata (source span).
357    pub meta: NodeMeta,
358    /// Per-column alignment from the delimiter row.
359    pub alignments: Vec<TableAlignment>,
360    /// All rows; the first is the header row.
361    pub rows: Vec<TableRow>,
362}
363
364/// The alignment of a [`Table`] column, from the `:---:` delimiter row.
365#[derive(Clone, Copy, Debug, Eq, PartialEq)]
366pub enum TableAlignment {
367    /// No explicit alignment: `---`.
368    None,
369    /// Left-aligned: `:---`.
370    Left,
371    /// Center-aligned: `:---:`.
372    Center,
373    /// Right-aligned: `---:`.
374    Right,
375}
376
377/// A single row of a [`Table`].
378#[derive(Clone, Debug, Eq, PartialEq)]
379pub struct TableRow {
380    /// Node metadata (source span).
381    pub meta: NodeMeta,
382    /// The row's cells.
383    pub cells: Vec<TableCell>,
384}
385
386/// A single cell of a [`TableRow`].
387#[derive(Clone, Debug, Eq, PartialEq)]
388pub struct TableCell {
389    /// Node metadata (source span).
390    pub meta: NodeMeta,
391    /// The cell's inline content.
392    pub children: Vec<Inline>,
393}
394
395/// A display math block. Source: `$$ … $$`.
396#[derive(Clone, Debug, Eq, PartialEq)]
397pub struct MathBlock {
398    /// Node metadata (source span).
399    pub meta: NodeMeta,
400    /// The literal math contents (between the `$$` fences).
401    pub value: String,
402}
403
404/// A leading frontmatter block. Source: `---` YAML or `+++` TOML at the top of
405/// the document.
406#[derive(Clone, Debug, Eq, PartialEq)]
407pub struct Frontmatter {
408    /// Node metadata (source span).
409    pub meta: NodeMeta,
410    /// Whether the frontmatter is YAML or TOML.
411    pub kind: FrontmatterKind,
412    /// The literal frontmatter contents (between the fences).
413    pub value: String,
414}
415
416/// The format of a [`Frontmatter`] block.
417#[derive(Clone, Copy, Debug, Eq, PartialEq)]
418pub enum FrontmatterKind {
419    /// YAML frontmatter, fenced by `---`.
420    Yaml,
421    /// TOML frontmatter, fenced by `+++`.
422    Toml,
423}
424
425/// An MDX ESM block: top-level `import`/`export` statements (distinct from directives).
426#[derive(Clone, Debug, Eq, PartialEq)]
427pub struct MdxEsm {
428    /// Node metadata (source span).
429    pub meta: NodeMeta,
430    /// The literal ESM source.
431    pub value: String,
432}
433
434/// A block-level MDX expression: `{ … }` (distinct from directives).
435#[derive(Clone, Debug, Eq, PartialEq)]
436pub struct MdxExpression {
437    /// Node metadata (source span).
438    pub meta: NodeMeta,
439    /// The literal expression source (between the braces).
440    pub value: String,
441}
442
443/// A block-level MDX JSX element (distinct from directives).
444#[derive(Clone, Debug, Eq, PartialEq)]
445pub struct MdxJsx {
446    /// Node metadata (source span).
447    pub meta: NodeMeta,
448    /// The literal JSX source.
449    pub value: String,
450}
451
452/// A leaf directive. Source: `::name[label]{attrs}` (a directive feature,
453/// not MDX).
454#[derive(Clone, Debug, Eq, PartialEq)]
455pub struct LeafDirective {
456    /// Node metadata (source span).
457    pub meta: NodeMeta,
458    /// The directive name following the `::`.
459    pub name: String,
460    /// The optional `[label]` inline content.
461    pub label: Vec<Inline>,
462    /// The optional `{attrs}` attributes.
463    pub attributes: Vec<DirectiveAttribute>,
464}
465
466/// A container directive. Source: `:::name[label]{attrs}` … `:::` (a directive
467/// feature, not MDX).
468#[derive(Clone, Debug, Eq, PartialEq)]
469pub struct ContainerDirective {
470    /// Node metadata (source span).
471    pub meta: NodeMeta,
472    /// The directive name following the `:::`.
473    pub name: String,
474    /// The optional `[label]` inline content.
475    pub label: Vec<Inline>,
476    /// The optional `{attrs}` attributes.
477    pub attributes: Vec<DirectiveAttribute>,
478    /// The directive's enclosed block content.
479    pub children: Vec<Block>,
480}
481
482/// An inline-level node: the leaf and span content inside blocks.
483#[derive(Clone, Debug, Eq, PartialEq)]
484pub enum Inline {
485    /// Literal text.
486    Text(Text),
487    /// A backslash escape such as `\*`.
488    Escape(Escape),
489    /// A character reference such as `&amp;` or `&#247;`.
490    CharacterReference(CharacterReference),
491    /// Emphasis: `*text*` or `_text_`.
492    Emphasis(Emphasis),
493    /// Strong emphasis: `**text**` or `__text__`.
494    Strong(Strong),
495    /// Underline: `__text__`/`___text___` (underscore extension).
496    Underline(Underline),
497    /// Strikethrough: `~~text~~`.
498    Delete(Delete),
499    /// A CriticMarkup-style insertion: `++text++`.
500    Insert(Insert),
501    /// A highlight / "mark" span: `==text==`.
502    Mark(Mark),
503    /// Subscript: `~x~`.
504    Subscript(Subscript),
505    /// Superscript: `^x^`.
506    Superscript(Superscript),
507    /// A spoiler span: `||text||`.
508    Spoiler(Spoiler),
509    /// An emoji-style shortcode: `:name:`.
510    Shortcode(Shortcode),
511    /// An inline code span: `` `code` ``.
512    Code(CodeInline),
513    /// An inline link: `[text](url)`.
514    Link(Link),
515    /// An inline image: `![alt](url)`.
516    Image(Image),
517    /// A reference link: `[text][label]`.
518    LinkReference(LinkReference),
519    /// A reference image: `![alt][label]`.
520    ImageReference(ImageReference),
521    /// An autolink: `<url>` or a GFM bare URL.
522    Autolink(Autolink),
523    /// Raw inline HTML such as `<span>`.
524    Html(HtmlInline),
525    /// A soft line break (a plain newline within a paragraph).
526    SoftBreak(SoftBreak),
527    /// A hard line break (`\` or two trailing spaces).
528    LineBreak(LineBreak),
529    /// Inline math: `$x$`.
530    Math(MathInline),
531    /// A footnote reference: `[^id]`.
532    FootnoteReference(FootnoteReference),
533    /// An inline footnote: `^[inline note]`.
534    InlineFootnote(InlineFootnote),
535    /// A wiki link: `[[target|label]]`.
536    WikiLink(WikiLink),
537    /// An inline MDX expression: `{ … }` (distinct from directives).
538    MdxExpression(MdxExpressionInline),
539    /// An inline MDX JSX element (distinct from directives).
540    MdxJsx(MdxJsxInline),
541    /// A text directive: `:name[label]{attrs}` (distinct from MDX).
542    TextDirective(TextDirective),
543}
544
545/// Literal text content.
546#[derive(Clone, Debug, Eq, PartialEq)]
547pub struct Text {
548    /// Node metadata (source span).
549    pub meta: NodeMeta,
550    /// The text value.
551    pub value: String,
552}
553
554/// A backslash escape such as `\*` or `\\`.
555#[derive(Clone, Debug, Eq, PartialEq)]
556pub struct Escape {
557    /// Node metadata (source span).
558    pub meta: NodeMeta,
559    /// The escaped (literal) character.
560    pub value: char,
561}
562
563/// A character reference such as `&amp;` or `&#247;`.
564#[derive(Clone, Debug, Eq, PartialEq)]
565pub struct CharacterReference {
566    /// Node metadata (source span).
567    pub meta: NodeMeta,
568    /// The reference as written, including `&` and `;` (e.g. `amp` for `&amp;`).
569    pub reference: String,
570    /// The resolved character value (e.g. `&` for `&amp;`).
571    pub value: String,
572}
573
574/// Emphasis (typically italic): `*text*` or `_text_`.
575#[derive(Clone, Debug, Eq, PartialEq)]
576pub struct Emphasis {
577    /// Node metadata (source span).
578    pub meta: NodeMeta,
579    /// The emphasized inline content.
580    pub children: Vec<Inline>,
581}
582
583/// Strong emphasis (typically bold): `**text**` or `__text__`.
584#[derive(Clone, Debug, Eq, PartialEq)]
585pub struct Strong {
586    /// Node metadata (source span).
587    pub meta: NodeMeta,
588    /// The strongly-emphasized inline content.
589    pub children: Vec<Inline>,
590}
591
592/// Underline (underscore extension): `__text__` or `___text___`.
593#[derive(Clone, Debug, Eq, PartialEq)]
594pub struct Underline {
595    /// Node metadata (source span).
596    pub meta: NodeMeta,
597    /// The underlined inline content.
598    pub children: Vec<Inline>,
599}
600
601/// Strikethrough: `~~text~~` (or single `~text~` when single-tilde is enabled).
602#[derive(Clone, Debug, Eq, PartialEq)]
603pub struct Delete {
604    /// Node metadata (source span).
605    pub meta: NodeMeta,
606    /// Whether the span used one or two tildes.
607    pub marker: DeleteMarker,
608    /// The struck-through inline content.
609    pub children: Vec<Inline>,
610}
611
612/// Which tilde run delimited a [`Delete`] span.
613#[derive(Clone, Copy, Debug, Eq, PartialEq)]
614pub enum DeleteMarker {
615    /// Single-tilde strikethrough: `~text~`.
616    SingleTilde,
617    /// Double-tilde strikethrough: `~~text~~`.
618    DoubleTilde,
619}
620
621/// A CriticMarkup-style insertion: `++text++`.
622#[derive(Clone, Debug, Eq, PartialEq)]
623pub struct Insert {
624    /// Node metadata (source span).
625    pub meta: NodeMeta,
626    /// The inserted inline content.
627    pub children: Vec<Inline>,
628}
629
630/// A highlight / "mark" span: `==text==`.
631#[derive(Clone, Debug, Eq, PartialEq)]
632pub struct Mark {
633    /// Node metadata (source span).
634    pub meta: NodeMeta,
635    /// The highlighted inline content.
636    pub children: Vec<Inline>,
637}
638
639/// Subscript: `~x~`.
640#[derive(Clone, Debug, Eq, PartialEq)]
641pub struct Subscript {
642    /// Node metadata (source span).
643    pub meta: NodeMeta,
644    /// The subscripted inline content.
645    pub children: Vec<Inline>,
646}
647
648/// Superscript: `^x^`.
649#[derive(Clone, Debug, Eq, PartialEq)]
650pub struct Superscript {
651    /// Node metadata (source span).
652    pub meta: NodeMeta,
653    /// The superscripted inline content.
654    pub children: Vec<Inline>,
655}
656
657/// A spoiler span: `||text||`.
658#[derive(Clone, Debug, Eq, PartialEq)]
659pub struct Spoiler {
660    /// Node metadata (source span).
661    pub meta: NodeMeta,
662    /// The hidden inline content.
663    pub children: Vec<Inline>,
664}
665
666/// An emoji-style shortcode: `:name:`.
667#[derive(Clone, Debug, Eq, PartialEq)]
668pub struct Shortcode {
669    /// Node metadata (source span).
670    pub meta: NodeMeta,
671    /// The shortcode name between the colons (e.g. `smile` for `:smile:`).
672    pub name: String,
673}
674
675/// An inline code span: `` `code` ``.
676#[derive(Clone, Debug, Eq, PartialEq)]
677pub struct CodeInline {
678    /// Node metadata (source span).
679    pub meta: NodeMeta,
680    /// The normalized code text (trimmed/collapsed per CommonMark).
681    pub value: String,
682    /// The raw text between the backtick fences, before normalization.
683    pub raw: String,
684    /// The number of backticks in the fence.
685    pub fence_length: usize,
686}
687
688/// An inline link: `[text](destination "title")`.
689#[derive(Clone, Debug, Eq, PartialEq)]
690pub struct Link {
691    /// Node metadata (source span).
692    pub meta: NodeMeta,
693    /// The link target URL.
694    pub destination: String,
695    /// How the destination was delimited (bare or `<…>`).
696    pub destination_kind: LinkDestinationKind,
697    /// The optional link title.
698    pub title: Option<String>,
699    /// How the title was quoted, if present.
700    pub title_kind: Option<LinkTitleKind>,
701    /// The link's inline content (the visible text).
702    pub children: Vec<Inline>,
703}
704
705/// An inline image: `![alt](destination "title")`.
706#[derive(Clone, Debug, Eq, PartialEq)]
707pub struct Image {
708    /// Node metadata (source span).
709    pub meta: NodeMeta,
710    /// The image source URL.
711    pub destination: String,
712    /// How the destination was delimited (bare or `<…>`).
713    pub destination_kind: LinkDestinationKind,
714    /// The optional image title.
715    pub title: Option<String>,
716    /// How the title was quoted, if present.
717    pub title_kind: Option<LinkTitleKind>,
718    /// The image's alt-text inline content.
719    pub alt: Vec<Inline>,
720}
721
722/// How a link/image destination was delimited in the source.
723#[derive(Clone, Copy, Debug, Eq, PartialEq)]
724pub enum LinkDestinationKind {
725    /// A bare destination: `(url)`.
726    Bare,
727    /// An angle-bracketed destination: `(<url>)`.
728    Angle,
729    /// No destination present: `()`.
730    Omitted,
731}
732
733/// How a link/image title was quoted in the source.
734#[derive(Clone, Copy, Debug, Eq, PartialEq)]
735pub enum LinkTitleKind {
736    /// Double-quoted: `"title"`.
737    DoubleQuote,
738    /// Single-quoted: `'title'`.
739    SingleQuote,
740    /// Parenthesized: `(title)`.
741    Paren,
742}
743
744/// A reference link: `[text][label]`, `[text][]`, or `[text]`.
745#[derive(Clone, Debug, Eq, PartialEq)]
746pub struct LinkReference {
747    /// Node metadata (source span).
748    pub meta: NodeMeta,
749    /// The normalized lookup key matching a [`Definition`].
750    pub identifier: String,
751    /// The label as written in the source.
752    pub label: String,
753    /// Whether the reference is full, collapsed, or shortcut form.
754    pub kind: ReferenceKind,
755    /// The link's inline content (the visible text).
756    pub children: Vec<Inline>,
757}
758
759/// A reference image: `![alt][label]`, `![alt][]`, or `![alt]`.
760#[derive(Clone, Debug, Eq, PartialEq)]
761pub struct ImageReference {
762    /// Node metadata (source span).
763    pub meta: NodeMeta,
764    /// The normalized lookup key matching a [`Definition`].
765    pub identifier: String,
766    /// The label as written in the source.
767    pub label: String,
768    /// Whether the reference is full, collapsed, or shortcut form.
769    pub kind: ReferenceKind,
770    /// The image's alt-text inline content.
771    pub alt: Vec<Inline>,
772}
773
774/// The form of a reference link/image.
775#[derive(Clone, Copy, Debug, Eq, PartialEq)]
776pub enum ReferenceKind {
777    /// Full reference: `[text][label]`.
778    Full,
779    /// Collapsed reference: `[label][]`.
780    Collapsed,
781    /// Shortcut reference: `[label]`.
782    Shortcut,
783}
784
785/// An autolink: `<url>` or a GFM bare URL.
786#[derive(Clone, Debug, Eq, PartialEq)]
787pub struct Autolink {
788    /// Node metadata (source span).
789    pub meta: NodeMeta,
790    /// The resolved link href.
791    pub destination: String,
792    /// Whether the link was angle-bracketed or a GFM literal.
793    pub kind: AutolinkKind,
794}
795
796/// Whether an [`Autolink`] is angle-bracketed or a GFM bare literal.
797#[derive(Clone, Debug, Eq, PartialEq)]
798pub enum AutolinkKind {
799    /// An angle-bracket autolink `<dest>`. The destination is the raw text
800    /// between the brackets; `>` is forbidden in the destination and the
801    /// serializer re-emits `<dest>`.
802    Angle,
803    /// A GFM literal autolink (bare `www.`/`http(s)://`/`mailto:`/`xmpp:` URL
804    /// or email). `original` is the raw source text that produced the link
805    /// (the visible label); `destination` is the synthesized href (e.g. a
806    /// `http://`/`mailto:` prefix may have been prepended). The serializer
807    /// re-emits `original`, which re-parses to the same literal.
808    GfmLiteral {
809        /// The raw source text that produced the link (the visible label).
810        original: String,
811    },
812}
813
814/// Raw inline HTML such as `<span>` or `</em>`.
815#[derive(Clone, Debug, Eq, PartialEq)]
816pub struct HtmlInline {
817    /// Node metadata (source span).
818    pub meta: NodeMeta,
819    /// The literal HTML source.
820    pub value: String,
821}
822
823/// A soft line break: a plain newline within a paragraph.
824#[derive(Clone, Debug, Eq, PartialEq)]
825pub struct SoftBreak {
826    /// Node metadata (source span).
827    pub meta: NodeMeta,
828}
829
830/// A hard line break: a trailing `\` or two trailing spaces.
831#[derive(Clone, Debug, Eq, PartialEq)]
832pub struct LineBreak {
833    /// Node metadata (source span).
834    pub meta: NodeMeta,
835    /// Which syntax produced the break.
836    pub kind: LineBreakKind,
837}
838
839/// Which syntax produced a hard [`LineBreak`].
840#[derive(Clone, Copy, Debug, Eq, PartialEq)]
841pub enum LineBreakKind {
842    /// A trailing backslash: `\`.
843    Backslash,
844    /// Two or more trailing spaces.
845    Spaces,
846}
847
848/// Which syntax delimited an inline [`MathInline`] span.
849#[derive(Clone, Copy, Debug, Eq, PartialEq)]
850pub enum MathInlineKind {
851    /// Dollar-fenced inline math (`$…$`, `$$…$$`, …); `dollars` is the fence
852    /// length (>=1). Dollar math always renders inline, while a 2-dollar fence
853    /// is conventionally treated as display elsewhere in the ecosystem.
854    Dollar {
855        /// The number of `$` characters in the fence (>=1).
856        dollars: u8,
857    },
858    /// Math-code span: `$`…`$`.
859    Code,
860}
861
862/// Inline math: `$x$`.
863#[derive(Clone, Debug, Eq, PartialEq)]
864pub struct MathInline {
865    /// Node metadata (source span).
866    pub meta: NodeMeta,
867    /// The literal math contents (between the fences).
868    pub value: String,
869    /// Which delimiter syntax was used.
870    pub kind: MathInlineKind,
871}
872
873/// A footnote reference: `[^id]`.
874#[derive(Clone, Debug, Eq, PartialEq)]
875pub struct FootnoteReference {
876    /// Node metadata (source span).
877    pub meta: NodeMeta,
878    /// The label as written in the source (the text after `^`).
879    pub label: String,
880    /// The normalized lookup key matching a [`FootnoteDefinition`].
881    pub identifier: String,
882}
883
884/// An inline footnote: `^[inline note]`.
885#[derive(Clone, Debug, Eq, PartialEq)]
886pub struct InlineFootnote {
887    /// Node metadata (source span).
888    pub meta: NodeMeta,
889    /// The footnote's inline content.
890    pub children: Vec<Inline>,
891}
892
893/// A wiki link: `[[target|label]]`.
894#[derive(Clone, Debug, Eq, PartialEq)]
895pub struct WikiLink {
896    /// Node metadata (source span).
897    pub meta: NodeMeta,
898    /// The link target (page name).
899    pub target: String,
900    /// The visible label.
901    pub label: String,
902    /// Whether the label appeared before or after the `|` in the source.
903    pub label_order: WikiLinkLabelOrder,
904}
905
906/// Whether a [`WikiLink`]'s label preceded or followed the `|` separator.
907#[derive(Clone, Copy, Debug, Eq, PartialEq)]
908pub enum WikiLinkLabelOrder {
909    /// Target then label: `[[target|label]]`.
910    AfterPipe,
911    /// Label then target: `[[label|target]]`.
912    BeforePipe,
913}
914
915/// An inline MDX expression: `{ … }` (distinct from directives).
916#[derive(Clone, Debug, Eq, PartialEq)]
917pub struct MdxExpressionInline {
918    /// Node metadata (source span).
919    pub meta: NodeMeta,
920    /// The literal expression source (between the braces).
921    pub value: String,
922}
923
924/// An inline MDX JSX element (distinct from directives).
925#[derive(Clone, Debug, Eq, PartialEq)]
926pub struct MdxJsxInline {
927    /// Node metadata (source span).
928    pub meta: NodeMeta,
929    /// The literal JSX source.
930    pub value: String,
931}
932
933/// A text directive. Source: `:name[label]{attrs}` (a directive feature,
934/// not MDX).
935#[derive(Clone, Debug, Eq, PartialEq)]
936pub struct TextDirective {
937    /// Node metadata (source span).
938    pub meta: NodeMeta,
939    /// The directive name following the `:`.
940    pub name: String,
941    /// The optional `[label]` inline content.
942    pub label: Vec<Inline>,
943    /// The optional `{attrs}` attributes.
944    pub attributes: Vec<DirectiveAttribute>,
945}
946
947/// One attribute of a directive's `{name=value}` block.
948#[derive(Clone, Debug, Eq, PartialEq)]
949pub struct DirectiveAttribute {
950    /// The attribute name.
951    pub name: String,
952    /// The attribute value, or `None` for a valueless attribute.
953    pub value: Option<String>,
954}
955
956// ---------------------------------------------------------------------------
957// Ergonomic accessors and a minimal construction layer.
958//
959// Every node carries a `meta: NodeMeta`, so `meta()`/`span()` are uniform across
960// the enums and free callers from writing an exhaustive match just to read a
961// span. `From`/`new` collapse the `Variant(Struct { meta, .. })` boilerplate for
962// hand-built ASTs; the raw struct literals remain available for full control.
963// ---------------------------------------------------------------------------
964
965macro_rules! impl_meta_accessors {
966    ($enum:ident { $($variant:ident),+ $(,)? }) => {
967        impl $enum {
968            /// Borrow this node's [`NodeMeta`].
969            pub fn meta(&self) -> &NodeMeta {
970                match self { $( $enum::$variant(node) => &node.meta, )+ }
971            }
972
973            /// This node's source span, if it carries one.
974            pub fn span(&self) -> Option<Span> {
975                self.meta().span
976            }
977        }
978    };
979}
980
981macro_rules! impl_from_variants {
982    ($enum:ident { $($variant:ident($ty:ty)),+ $(,)? }) => {
983        $(
984            impl From<$ty> for $enum {
985                fn from(node: $ty) -> Self {
986                    $enum::$variant(node)
987                }
988            }
989        )+
990    };
991}
992
993impl_meta_accessors!(Block {
994    Paragraph,
995    Heading,
996    ThematicBreak,
997    BlockQuote,
998    Alert,
999    List,
1000    DescriptionList,
1001    CodeBlock,
1002    HtmlBlock,
1003    HtmlContainer,
1004    Definition,
1005    FootnoteDefinition,
1006    Table,
1007    MathBlock,
1008    Frontmatter,
1009    MdxEsm,
1010    MdxExpression,
1011    MdxJsx,
1012    LeafDirective,
1013    ContainerDirective,
1014});
1015
1016impl_from_variants!(Block {
1017    Paragraph(Paragraph), Heading(Heading), ThematicBreak(ThematicBreak),
1018    BlockQuote(BlockQuote), Alert(Alert), List(List), DescriptionList(DescriptionList),
1019    CodeBlock(CodeBlock), HtmlBlock(HtmlBlock), Definition(Definition),
1020    HtmlContainer(HtmlContainer),
1021    FootnoteDefinition(FootnoteDefinition), Table(Table), MathBlock(MathBlock),
1022    Frontmatter(Frontmatter), MdxEsm(MdxEsm), MdxExpression(MdxExpression),
1023    MdxJsx(MdxJsx), LeafDirective(LeafDirective), ContainerDirective(ContainerDirective),
1024});
1025
1026impl_meta_accessors!(Inline {
1027    Text,
1028    Escape,
1029    CharacterReference,
1030    Emphasis,
1031    Strong,
1032    Underline,
1033    Delete,
1034    Insert,
1035    Mark,
1036    Subscript,
1037    Superscript,
1038    Spoiler,
1039    Shortcode,
1040    Code,
1041    Link,
1042    Image,
1043    LinkReference,
1044    ImageReference,
1045    Autolink,
1046    Html,
1047    SoftBreak,
1048    LineBreak,
1049    Math,
1050    FootnoteReference,
1051    InlineFootnote,
1052    WikiLink,
1053    MdxExpression,
1054    MdxJsx,
1055    TextDirective,
1056});
1057
1058impl_from_variants!(Inline {
1059    Text(Text), Escape(Escape), CharacterReference(CharacterReference),
1060    Emphasis(Emphasis), Strong(Strong), Underline(Underline), Delete(Delete),
1061    Insert(Insert), Mark(Mark), Subscript(Subscript), Superscript(Superscript),
1062    Spoiler(Spoiler), Shortcode(Shortcode), Code(CodeInline), Link(Link), Image(Image),
1063    LinkReference(LinkReference), ImageReference(ImageReference), Autolink(Autolink),
1064    Html(HtmlInline), SoftBreak(SoftBreak), LineBreak(LineBreak), Math(MathInline),
1065    FootnoteReference(FootnoteReference), InlineFootnote(InlineFootnote), WikiLink(WikiLink),
1066    MdxExpression(MdxExpressionInline), MdxJsx(MdxJsxInline), TextDirective(TextDirective),
1067});
1068
1069impl Inline {
1070    /// The inline subtree of this node, or an empty slice for a leaf. Covers the
1071    /// `alt`/`label` fields uniformly, so a generic walker never silently skips
1072    /// an image's alt text or a directive's label.
1073    pub fn children(&self) -> &[Inline] {
1074        match self {
1075            Inline::Emphasis(n) => &n.children,
1076            Inline::Strong(n) => &n.children,
1077            Inline::Underline(n) => &n.children,
1078            Inline::Delete(n) => &n.children,
1079            Inline::Insert(n) => &n.children,
1080            Inline::Mark(n) => &n.children,
1081            Inline::Subscript(n) => &n.children,
1082            Inline::Superscript(n) => &n.children,
1083            Inline::Spoiler(n) => &n.children,
1084            Inline::Link(n) => &n.children,
1085            Inline::Image(n) => &n.alt,
1086            Inline::LinkReference(n) => &n.children,
1087            Inline::ImageReference(n) => &n.alt,
1088            Inline::InlineFootnote(n) => &n.children,
1089            Inline::TextDirective(n) => &n.label,
1090            _ => &[],
1091        }
1092    }
1093}
1094
1095impl Text {
1096    /// A text node with the given string value.
1097    pub fn new(value: impl Into<String>) -> Self {
1098        Self {
1099            meta: NodeMeta::default(),
1100            value: value.into(),
1101        }
1102    }
1103}
1104
1105impl From<&str> for Text {
1106    fn from(value: &str) -> Self {
1107        Text::new(value)
1108    }
1109}
1110
1111impl From<String> for Text {
1112    fn from(value: String) -> Self {
1113        Text::new(value)
1114    }
1115}
1116
1117impl Paragraph {
1118    /// A paragraph from any iterator of inline-convertible children.
1119    pub fn new<I, T>(children: I) -> Self
1120    where
1121        I: IntoIterator<Item = T>,
1122        T: Into<Inline>,
1123    {
1124        Self {
1125            meta: NodeMeta::default(),
1126            children: children.into_iter().map(Into::into).collect(),
1127        }
1128    }
1129}
1130
1131impl Heading {
1132    /// An ATX heading of the given depth.
1133    pub fn new<I, T>(depth: u8, children: I) -> Self
1134    where
1135        I: IntoIterator<Item = T>,
1136        T: Into<Inline>,
1137    {
1138        Self {
1139            meta: NodeMeta::default(),
1140            depth,
1141            kind: HeadingKind::Atx,
1142            children: children.into_iter().map(Into::into).collect(),
1143        }
1144    }
1145}
1146
1147impl Link {
1148    /// A bare-destination link with no title.
1149    pub fn new<I, T>(destination: impl Into<String>, children: I) -> Self
1150    where
1151        I: IntoIterator<Item = T>,
1152        T: Into<Inline>,
1153    {
1154        Self {
1155            meta: NodeMeta::default(),
1156            destination: destination.into(),
1157            destination_kind: LinkDestinationKind::Bare,
1158            title: None,
1159            title_kind: None,
1160            children: children.into_iter().map(Into::into).collect(),
1161        }
1162    }
1163}
1164
1165impl CodeInline {
1166    /// An inline code span with a single-backtick fence.
1167    pub fn new(value: impl Into<String>) -> Self {
1168        let value = value.into();
1169        Self {
1170            meta: NodeMeta::default(),
1171            raw: value.clone(),
1172            value,
1173            fence_length: 1,
1174        }
1175    }
1176}
1177
1178impl List {
1179    /// A tight, dash-delimited bullet list.
1180    pub fn new<I>(children: I) -> Self
1181    where
1182        I: IntoIterator<Item = ListItem>,
1183    {
1184        Self {
1185            meta: NodeMeta::default(),
1186            ordered: false,
1187            start: None,
1188            delimiter: ListDelimiter::Dash,
1189            tight: true,
1190            children: children.into_iter().collect(),
1191        }
1192    }
1193}