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