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/// `` 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}