Skip to main content

oxc_markdown_parser/
ast.rs

1//! AST node definitions.
2//!
3//! Node taxonomy (heading / paragraph / list / html / emphasis / …),
4//! sibling order and spans follow CommonMark's grammar;
5//! a printer's dispatch is written per this unit.
6//!
7//! Beyond that the shapes deliberately diverge from mdast:
8//!
9//! - No cooked values: consumers slice the original source through [`Span`]s.
10//!   Inline node spans are original-source offsets, never rebuilt-buffer ones.
11//! - Style facts a printer would otherwise re-derive by peeking at the source
12//!   (markers, fence chars, reference kinds, break kinds, …) are first-class fields.
13//! - Formatter policy (sentence splitting, CJK classification, alignment judgment) stays out;
14//!   the AST records lexical facts only.
15//!
16//! Blank lines are not stored on nodes: `ParserReturn::blanks` lists them once,
17//! and sibling spans locate the ones between two blocks (`List::tight` is computed from it).
18
19use oxc_allocator::{ArenaBox, ArenaVec};
20
21use crate::pos::{Segment, Span};
22
23/// The document.
24#[derive(Debug)]
25pub struct Root<'a> {
26    pub children: ArenaVec<'a, Block<'a>>,
27    pub span: Span,
28}
29
30/// Flow (block-level) content.
31// Variant names mirror the node struct names;
32// the Block/Inline suffixes disambiguate the flow/phrasing pairs (CodeBlock vs CodeSpan, …).
33#[expect(clippy::enum_variant_names)]
34#[derive(Debug)]
35pub enum Block<'a> {
36    Paragraph(ArenaBox<'a, Paragraph<'a>>),
37    Heading(ArenaBox<'a, Heading<'a>>),
38    ThematicBreak(ThematicBreak),
39    CodeBlock(ArenaBox<'a, CodeBlock<'a>>),
40    HtmlBlock(ArenaBox<'a, HtmlBlock<'a>>),
41    Blockquote(ArenaBox<'a, Blockquote<'a>>),
42    List(ArenaBox<'a, List<'a>>),
43    Definition(ArenaBox<'a, Definition<'a>>),
44    Table(ArenaBox<'a, Table<'a>>),
45    FootnoteDefinition(ArenaBox<'a, FootnoteDefinition<'a>>),
46    MathBlock(ArenaBox<'a, MathBlock<'a>>),
47    /// `{% … %}` / `{{ … }}` standing alone at flow level;
48    /// kept verbatim so it can't be absorbed into neighboring blocks.
49    Liquid(ArenaBox<'a, Liquid<'a>>),
50    ContainerDirective(ArenaBox<'a, ContainerDirective<'a>>),
51    MdxEsm(MdxEsm),
52    MdxExpression(MdxExpression),
53    MdxJsx(ArenaBox<'a, MdxJsxFlow<'a>>),
54}
55
56impl Block<'_> {
57    pub fn span(&self) -> Span {
58        match self {
59            Block::Paragraph(n) => n.span,
60            Block::Heading(n) => n.span,
61            Block::ThematicBreak(n) => n.span,
62            Block::CodeBlock(n) => n.span,
63            Block::HtmlBlock(n) => n.span,
64            Block::Blockquote(n) => n.span,
65            Block::List(n) => n.span,
66            Block::Definition(n) => n.span,
67            Block::Table(n) => n.span,
68            Block::FootnoteDefinition(n) => n.span,
69            Block::MathBlock(n) => n.span,
70            Block::Liquid(n) => n.span,
71            Block::ContainerDirective(n) => n.span,
72            Block::MdxEsm(n) => n.span,
73            Block::MdxExpression(n) => n.span,
74            Block::MdxJsx(n) => n.span,
75        }
76    }
77}
78
79/// Phrasing (inline) content.
80// Same policy as [`Block`]: HtmlInline pairs with HtmlBlock.
81#[expect(clippy::enum_variant_names)]
82#[derive(Debug)]
83pub enum Inline<'a> {
84    Text(Text),
85    /// A soft line break (newline inside a paragraph).
86    /// Rendered as `\n`; proseWrap policy decides its fate downstream.
87    SoftBreak(SoftBreak),
88    Emphasis(ArenaBox<'a, Emphasis<'a>>),
89    Strong(ArenaBox<'a, Strong<'a>>),
90    Strikethrough(ArenaBox<'a, Strikethrough<'a>>),
91    CodeSpan(ArenaBox<'a, CodeSpan<'a>>),
92    Link(ArenaBox<'a, Link<'a>>),
93    Image(ArenaBox<'a, Image<'a>>),
94    Autolink(Autolink),
95    /// GFM bare URL / email, printed verbatim.
96    AutolinkLiteral(AutolinkLiteral),
97    HtmlInline(ArenaBox<'a, HtmlInline<'a>>),
98    HardBreak(HardBreak),
99    FootnoteReference(ArenaBox<'a, FootnoteReference>),
100    /// `$ … $`, printed verbatim.
101    MathSpan(ArenaBox<'a, MathSpan<'a>>),
102    /// `[[target]]`, printed verbatim.
103    WikiLink(WikiLink),
104    /// `{% … %}` / `{{ … }}` inside text, kept verbatim.
105    Liquid(ArenaBox<'a, Liquid<'a>>),
106    MdxExpression(MdxExpression),
107    MdxJsx(ArenaBox<'a, MdxJsxText<'a>>),
108}
109
110impl Inline<'_> {
111    pub fn span(&self) -> Span {
112        match self {
113            Inline::Text(n) => n.span,
114            Inline::SoftBreak(n) => n.span,
115            Inline::Emphasis(n) => n.span,
116            Inline::Strong(n) => n.span,
117            Inline::Strikethrough(n) => n.span,
118            Inline::CodeSpan(n) => n.span,
119            Inline::Link(n) => n.span,
120            Inline::Image(n) => n.span,
121            Inline::Autolink(n) => n.span,
122            Inline::AutolinkLiteral(n) => n.span,
123            Inline::HtmlInline(n) => n.span,
124            Inline::HardBreak(n) => n.span,
125            Inline::FootnoteReference(n) => n.span,
126            Inline::MathSpan(n) => n.span,
127            Inline::WikiLink(n) => n.span,
128            Inline::Liquid(n) => n.span,
129            Inline::MdxExpression(n) => n.span,
130            Inline::MdxJsx(n) => n.span,
131        }
132    }
133}
134
135#[derive(Debug)]
136pub struct Paragraph<'a> {
137    pub children: ArenaVec<'a, Inline<'a>>,
138    /// Source lines that entered this paragraph as lazy continuations
139    /// (unprefixed lines inside a blockquote/list).
140    /// A printer must not merge or re-wrap across these: doing so changes meaning.
141    pub lazy_lines: ArenaVec<'a, Span>,
142    pub span: Span,
143}
144
145#[derive(Debug)]
146pub struct Heading<'a> {
147    /// 1–6 for ATX, 1–2 for setext.
148    pub level: u8,
149    pub kind: HeadingKind<'a>,
150    pub children: ArenaVec<'a, Inline<'a>>,
151    pub span: Span,
152}
153
154#[derive(Debug)]
155pub enum HeadingKind<'a> {
156    Atx,
157    /// The underline is printed verbatim (its length is preserved).
158    Setext {
159        underline: Span,
160        /// Source lines that entered the underlying paragraph as lazy continuations,
161        /// as on [`Paragraph::lazy_lines`].
162        /// A printer keeping the setext form must not merge or re-wrap across these.
163        lazy_lines: ArenaVec<'a, Span>,
164    },
165}
166
167#[derive(Clone, Copy, Debug)]
168pub struct ThematicBreak {
169    pub span: Span,
170}
171
172#[derive(Debug)]
173pub struct CodeBlock<'a> {
174    pub kind: CodeBlockKind,
175    /// Logical content lines (container prefixes and fence/code indent stripped).
176    /// The printer recomputes the minimum fence length from them.
177    pub lines: ArenaVec<'a, Segment>,
178    pub span: Span,
179}
180
181#[derive(Clone, Copy, Debug)]
182pub enum CodeBlockKind {
183    Fenced {
184        /// `` ` `` or `~`.
185        fence: u8,
186        /// The info string's first word (mdast `lang`), if any.
187        lang: Option<Span>,
188        /// The rest of the info string after `lang` and the whitespace (mdast `meta`), if any.
189        meta: Option<Span>,
190    },
191    /// Preserved as indented (never converted to fenced).
192    Indented,
193}
194
195/// A raw HTML block. Contents are never parsed or formatted.
196#[derive(Debug)]
197pub struct HtmlBlock<'a> {
198    /// CommonMark HTML block type 1–7.
199    /// Type 1 (`<pre>`/`<script>`/`<style>`/ `<textarea>`) runs to its closing tag;
200    /// the rest end at a blank line,
201    /// which is how markdown content interleaves between unbalanced tags.
202    pub kind: u8,
203    /// Logical content lines, verbatim.
204    pub lines: ArenaVec<'a, Segment>,
205    /// Whether the block's final line ending belongs to its content
206    /// (micromark: `html.value` ends with a line ending).
207    /// Only types 1–5, in two cases.
208    /// The block was still open at EOF (or at a directive's closing fence), outside any blockquote.
209    /// The line that closed its container opened a new one (list item, blockquote, footnote definition).
210    /// Types 6–7 end at their last content line, so never.
211    pub trailing_newline: bool,
212    pub span: Span,
213}
214
215#[derive(Debug)]
216pub struct Blockquote<'a> {
217    pub children: ArenaVec<'a, Block<'a>>,
218    pub span: Span,
219}
220
221#[derive(Debug)]
222pub struct List<'a> {
223    pub marker: ListMarker,
224    /// Loose lists render children as paragraphs;
225    /// blank-line placement around items decides this at parse time.
226    pub tight: bool,
227    pub children: ArenaVec<'a, ListItem<'a>>,
228    pub span: Span,
229}
230
231#[derive(Clone, Copy, Debug)]
232pub enum ListMarker {
233    /// `-` / `*` / `+`.
234    Bullet { marker: u8 },
235    /// `.` / `)` after the number.
236    Ordered { delimiter: u8 },
237}
238
239#[derive(Debug)]
240pub struct ListItem<'a> {
241    /// The marker itself: `-`, or the full `123.`.
242    /// Ordered numbers stay raw
243    /// (CommonMark caps them at 999,999,999; arithmetic on them overflows).
244    pub marker: Span,
245    /// Spaces between the marker and the content, for aligned-list judgment.
246    pub padding: u8,
247    /// GFM task list checkbox, when the item starts with one.
248    /// The first paragraph begins after the checkbox and the whitespace before same-line content;
249    /// when only whitespace follows on that line it stays in the paragraph,
250    /// as the line's soft or hard break.
251    pub checkbox: Option<TaskCheckbox>,
252    /// A blank line separates two of this item's children (mdast's `listItem.spread`).
253    /// Looseness is judged per item downstream (Prettier: this, or a blank line before the next item),
254    /// while [`List::tight`] is the whole list's cmark verdict.
255    pub spread: bool,
256    pub children: ArenaVec<'a, Block<'a>>,
257    pub span: Span,
258}
259
260/// `[ ]`, `[x]` or `[X]` (the raw span keeps the case).
261#[derive(Clone, Copy, Debug)]
262pub struct TaskCheckbox {
263    pub span: Span,
264    pub checked: bool,
265}
266
267/// A link reference definition: `[label]: dest "title"`.
268///
269/// Label and title may span lines,
270/// and a multi-line span would include the container prefixes between them,
271/// so both are logical-line pieces (see [`Segment::join`]).
272/// Normalize the label from the joined pieces;
273/// a title's continuation pieces keep their leading whitespace raw (micromark strips all of it).
274#[derive(Debug)]
275pub struct Definition<'a> {
276    /// Between the brackets.
277    pub label: ArenaVec<'a, Segment>,
278    pub destination: Destination,
279    /// Including the quotes.
280    pub title: Option<ArenaVec<'a, Segment>>,
281    pub span: Span,
282}
283
284#[derive(Debug)]
285pub struct Table<'a> {
286    /// One entry per column of the delimiter row.
287    /// Alignments beyond the header row's cell count are dropped (GFM).
288    pub align: ArenaVec<'a, TableAlign>,
289    /// The first row is the header.
290    pub children: ArenaVec<'a, TableRow<'a>>,
291    pub span: Span,
292}
293
294#[derive(Clone, Copy, Debug, PartialEq, Eq)]
295pub enum TableAlign {
296    None,
297    Left,
298    Center,
299    Right,
300}
301
302#[derive(Debug)]
303pub struct TableRow<'a> {
304    pub children: ArenaVec<'a, TableCell<'a>>,
305    pub span: Span,
306}
307
308#[derive(Debug)]
309pub struct TableCell<'a> {
310    pub children: ArenaVec<'a, Inline<'a>>,
311    pub span: Span,
312}
313
314#[derive(Debug)]
315pub struct FootnoteDefinition<'a> {
316    pub label: Span,
317    pub children: ArenaVec<'a, Block<'a>>,
318    pub span: Span,
319}
320
321/// `$$ … $$` display math, content is kept raw.
322/// As with code fences, the fence length is not stored
323/// (printers normalize to `$$`; the `$` run at `span.start` gives it if ever needed).
324#[derive(Debug)]
325pub struct MathBlock<'a> {
326    /// Raw text after the opening fence (`$$meta`), leading whitespace stripped.
327    /// Never contains `$`.
328    pub meta: Option<Span>,
329    /// Logical content lines (container prefixes and the opening fence's indent stripped),
330    /// fence lines excluded.
331    pub lines: ArenaVec<'a, Segment>,
332    pub span: Span,
333}
334
335/// A container directive:
336///
337/// ```markdown
338/// :::note[label]{.class}
339/// children
340/// :::
341/// ```
342#[derive(Debug)]
343pub struct ContainerDirective<'a> {
344    /// The whole opening fence line, kept verbatim.
345    /// Dialects disagree on its grammar (remark-directive vs markdown-it-container),
346    /// so it is never normalized.
347    pub opening: Span,
348    /// The closing fence line, if the directive was explicitly closed.
349    /// Nesting uses longer fences; as with code fences, the length is not stored
350    /// (the `:` run at `opening.start` gives it).
351    pub closing: Option<Span>,
352    pub children: ArenaVec<'a, Block<'a>>,
353    pub span: Span,
354}
355
356#[derive(Clone, Copy, Debug)]
357pub struct Text {
358    pub span: Span,
359    /// Every byte is ASCII.
360    /// Contains CJK characters.
361    /// Classification (CJ vs K vs punctuation) is formatter policy and happens downstream.
362    pub contains_cjk: bool,
363}
364
365#[derive(Debug)]
366pub struct Emphasis<'a> {
367    /// `*` or `_`. Normalization is context-dependent,
368    /// so the source marker is a required input, not a derivable one.
369    pub marker: u8,
370    pub children: ArenaVec<'a, Inline<'a>>,
371    pub span: Span,
372}
373
374#[derive(Debug)]
375pub struct Strong<'a> {
376    /// `*` or `_`.
377    pub marker: u8,
378    pub children: ArenaVec<'a, Inline<'a>>,
379    pub span: Span,
380}
381
382/// GFM `~~x~~` (`~x~` too with `gfm_strikethrough_single_tilde`).
383#[derive(Debug)]
384pub struct Strikethrough<'a> {
385    /// 1 or 2.
386    pub tildes: u8,
387    pub children: ArenaVec<'a, Inline<'a>>,
388    pub span: Span,
389}
390
391#[derive(Debug)]
392pub struct CodeSpan<'a> {
393    /// Content between the backtick runs, one segment per line
394    /// (a multi-line code span crosses container prefixes, so a single span can't cover it;
395    /// a partially consumed tab leaves virtual spaces in `padding`).
396    /// The printer recomputes the minimum backtick run length from these.
397    /// Inside a table cell a `|` is `\|` in the source and stays so (nothing to escape).
398    pub pieces: ArenaVec<'a, Segment>,
399    pub span: Span,
400}
401
402/// `[text](dest "title")` or `[text][label]` / `[text][]` / `[text]`.
403#[derive(Debug)]
404pub struct Link<'a> {
405    pub kind: LinkKind<'a>,
406    pub children: ArenaVec<'a, Inline<'a>>,
407    pub span: Span,
408}
409
410/// `![alt](dest "title")` and the reference forms.
411#[derive(Debug)]
412pub struct Image<'a> {
413    pub kind: LinkKind<'a>,
414    /// The raw text between `![` and `]`, as logical-line pieces
415    /// (a printer reproduces it as written; mdast cooks it to `alt`).
416    pub alt: ArenaVec<'a, Segment>,
417    /// The alt text parsed as inline content.
418    /// Nested link/image syntax is preserved here, not cooked.
419    pub children: ArenaVec<'a, Inline<'a>>,
420    pub span: Span,
421}
422
423/// Titles and labels are logical-line pieces for the same reason as on [`Definition`].
424#[derive(Debug)]
425pub enum LinkKind<'a> {
426    Inline {
427        destination: Destination,
428        /// Including the quotes.
429        title: Option<ArenaVec<'a, Segment>>,
430    },
431    Reference {
432        kind: ReferenceKind,
433        /// The text the reference resolves through:
434        /// the second bracket's interior for `Full`, the link text for `Collapsed`/`Shortcut`.
435        label: ArenaVec<'a, Segment>,
436    },
437}
438
439/// Reference-link form.
440/// Only `Full` is reformatted; the others are printed verbatim.
441#[derive(Clone, Copy, Debug, PartialEq, Eq)]
442pub enum ReferenceKind {
443    /// `[text][label]`
444    Full,
445    /// `[text][]`
446    Collapsed,
447    /// `[text]`
448    Shortcut,
449}
450
451/// A link/image destination, excluding the title.
452#[derive(Clone, Copy, Debug)]
453pub struct Destination {
454    /// Covers the `<`/`>` when angle-bracketed.
455    pub span: Span,
456    /// `<dest>` form. Whether brackets are needed on output is a lexical judgment
457    /// (spaces, `(`, `<`, `\` …), computed from the raw span.
458    pub angle_bracketed: bool,
459}
460
461/// `<https://example.com>` / `<user@example.com>`.
462#[derive(Clone, Copy, Debug)]
463pub struct Autolink {
464    pub span: Span,
465    /// Email autolinks render with an implied `mailto:` that must not leak into output.
466    pub email: bool,
467}
468
469/// GFM autolink literal (`www.example.com`, bare `https://…` / email).
470#[derive(Clone, Copy, Debug)]
471pub struct AutolinkLiteral {
472    pub span: Span,
473}
474
475/// A raw inline HTML tag or comment.
476/// The tag is a verbatim token; text around it is markdown.
477#[derive(Debug)]
478pub struct HtmlInline<'a> {
479    /// Verbatim content, one segment per line, leading whitespace of continuation lines included
480    /// (what mdast's `html.value` holds; only micromark's HTML compiler strips up to 3 columns of it).
481    pub pieces: ArenaVec<'a, Segment>,
482    pub span: Span,
483}
484
485#[derive(Clone, Copy, Debug)]
486pub struct SoftBreak {
487    /// Zero-width, at the first content byte of the following line
488    /// (past the line ending and any container prefix).
489    /// The line ending itself is not covered: it lies between the two sibling spans.
490    pub span: Span,
491}
492
493#[derive(Clone, Copy, Debug)]
494pub struct HardBreak {
495    pub kind: HardBreakKind,
496    pub span: Span,
497}
498
499#[derive(Clone, Copy, Debug, PartialEq, Eq)]
500pub enum HardBreakKind {
501    /// Two-plus trailing spaces before the newline.
502    Spaces,
503    /// `\` before the newline.
504    Backslash,
505}
506
507/// GFM `[^label]`.
508#[derive(Clone, Copy, Debug)]
509pub struct FootnoteReference {
510    pub label: Span,
511    pub span: Span,
512}
513
514/// `$ … $` inline math.
515/// The whole construct, `$` sequences included, is kept raw;
516/// content is never trimmed or decoded.
517#[derive(Debug)]
518pub struct MathSpan<'a> {
519    /// Verbatim construct, one segment per line
520    /// (a multi-line span crosses container prefixes, so a single span can't cover it).
521    pub pieces: ArenaVec<'a, Segment>,
522    pub span: Span,
523}
524
525/// `[[target]]`, alias-less.
526/// Always single-line; the target is `span + 2 .. span - 2`, kept raw.
527#[derive(Clone, Copy, Debug)]
528pub struct WikiLink {
529    pub span: Span,
530}
531
532/// A `{% … %}` / `{{ … }}` template tag, block-level or inline.
533/// Never interpreted: the whole construct, delimiters included, is kept verbatim.
534#[derive(Debug)]
535pub struct Liquid<'a> {
536    /// Verbatim construct, one segment per line (multi-line tags cross container prefixes).
537    pub pieces: ArenaVec<'a, Segment>,
538    pub span: Span,
539}
540
541/// An MDX `import`/`export` statement.
542/// The parser only establishes the boundary (via the host's JS parser callback);
543/// the content stays raw for embedded formatting.
544#[derive(Clone, Copy, Debug)]
545pub struct MdxEsm {
546    pub span: Span,
547}
548
549/// An MDX `{expr}`.
550/// Boundary only; the expression stays raw.
551#[derive(Clone, Copy, Debug)]
552pub struct MdxExpression {
553    pub span: Span,
554}
555
556/// An MDX JSX element at flow level.
557/// Tags stay raw for embedded formatting; children are markdown blocks.
558#[derive(Debug)]
559pub struct MdxJsxFlow<'a> {
560    /// The opening (or self-closing) tag, raw.
561    pub opening: Span,
562    /// The closing tag, raw. `None` when self-closing.
563    pub closing: Option<Span>,
564    pub children: ArenaVec<'a, Block<'a>>,
565    pub span: Span,
566}
567
568/// An MDX JSX element in text.
569/// Children are inline markdown.
570#[derive(Debug)]
571pub struct MdxJsxText<'a> {
572    pub opening: Span,
573    pub closing: Option<Span>,
574    pub children: ArenaVec<'a, Inline<'a>>,
575    pub span: Span,
576}