oxc-markdown-parser 0.0.3

A CommonMark + GFM parser that produces a span-faithful, style-preserving typed AST.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
//! AST node definitions.
//!
//! Node taxonomy (heading / paragraph / list / html / emphasis / …),
//! sibling order and spans follow CommonMark's grammar;
//! a printer's dispatch is written per this unit.
//!
//! Beyond that the shapes deliberately diverge from mdast:
//!
//! - No cooked values: consumers slice the original source through [`Span`]s.
//!   Inline node spans are original-source offsets, never rebuilt-buffer ones.
//! - Style facts a printer would otherwise re-derive by peeking at the source
//!   (markers, fence chars, reference kinds, break kinds, …) are first-class fields.
//! - Formatter policy (sentence splitting, CJK classification, alignment judgment) stays out;
//!   the AST records lexical facts only.
//!
//! Blank lines are not stored on nodes: `ParserReturn::blanks` lists them once,
//! and sibling spans locate the ones between two blocks (`List::tight` is computed from it).

use oxc_allocator::{ArenaBox, ArenaVec};

use crate::pos::{Segment, Span};

/// The document.
#[derive(Debug)]
pub struct Root<'a> {
    pub children: ArenaVec<'a, Block<'a>>,
    pub span: Span,
}

/// Flow (block-level) content.
// Variant names mirror the node struct names;
// the Block/Inline suffixes disambiguate the flow/phrasing pairs (CodeBlock vs CodeSpan, …).
#[expect(clippy::enum_variant_names)]
#[derive(Debug)]
pub enum Block<'a> {
    Paragraph(ArenaBox<'a, Paragraph<'a>>),
    Heading(ArenaBox<'a, Heading<'a>>),
    ThematicBreak(ThematicBreak),
    CodeBlock(ArenaBox<'a, CodeBlock<'a>>),
    HtmlBlock(ArenaBox<'a, HtmlBlock<'a>>),
    Blockquote(ArenaBox<'a, Blockquote<'a>>),
    List(ArenaBox<'a, List<'a>>),
    Definition(ArenaBox<'a, Definition<'a>>),
    Table(ArenaBox<'a, Table<'a>>),
    FootnoteDefinition(ArenaBox<'a, FootnoteDefinition<'a>>),
    MathBlock(ArenaBox<'a, MathBlock<'a>>),
    /// `{% … %}` / `{{ … }}` standing alone at flow level;
    /// kept verbatim so it can't be absorbed into neighboring blocks.
    Liquid(ArenaBox<'a, Liquid<'a>>),
    ContainerDirective(ArenaBox<'a, ContainerDirective<'a>>),
    MdxEsm(MdxEsm),
    MdxExpression(MdxExpression),
    MdxJsx(ArenaBox<'a, MdxJsxFlow<'a>>),
}

impl Block<'_> {
    pub fn span(&self) -> Span {
        match self {
            Block::Paragraph(n) => n.span,
            Block::Heading(n) => n.span,
            Block::ThematicBreak(n) => n.span,
            Block::CodeBlock(n) => n.span,
            Block::HtmlBlock(n) => n.span,
            Block::Blockquote(n) => n.span,
            Block::List(n) => n.span,
            Block::Definition(n) => n.span,
            Block::Table(n) => n.span,
            Block::FootnoteDefinition(n) => n.span,
            Block::MathBlock(n) => n.span,
            Block::Liquid(n) => n.span,
            Block::ContainerDirective(n) => n.span,
            Block::MdxEsm(n) => n.span,
            Block::MdxExpression(n) => n.span,
            Block::MdxJsx(n) => n.span,
        }
    }
}

/// Phrasing (inline) content.
// Same policy as [`Block`]: HtmlInline pairs with HtmlBlock.
#[expect(clippy::enum_variant_names)]
#[derive(Debug)]
pub enum Inline<'a> {
    Text(Text),
    /// A soft line break (newline inside a paragraph).
    /// Rendered as `\n`; proseWrap policy decides its fate downstream.
    SoftBreak(SoftBreak),
    Emphasis(ArenaBox<'a, Emphasis<'a>>),
    Strong(ArenaBox<'a, Strong<'a>>),
    Strikethrough(ArenaBox<'a, Strikethrough<'a>>),
    CodeSpan(ArenaBox<'a, CodeSpan<'a>>),
    Link(ArenaBox<'a, Link<'a>>),
    Image(ArenaBox<'a, Image<'a>>),
    Autolink(Autolink),
    /// GFM bare URL / email, printed verbatim.
    AutolinkLiteral(AutolinkLiteral),
    HtmlInline(ArenaBox<'a, HtmlInline<'a>>),
    HardBreak(HardBreak),
    FootnoteReference(ArenaBox<'a, FootnoteReference>),
    /// `$ … $`, printed verbatim.
    MathSpan(ArenaBox<'a, MathSpan<'a>>),
    /// `[[target]]`, printed verbatim.
    WikiLink(WikiLink),
    /// `{% … %}` / `{{ … }}` inside text, kept verbatim.
    Liquid(ArenaBox<'a, Liquid<'a>>),
    MdxExpression(MdxExpression),
    MdxJsx(ArenaBox<'a, MdxJsxText<'a>>),
}

impl Inline<'_> {
    pub fn span(&self) -> Span {
        match self {
            Inline::Text(n) => n.span,
            Inline::SoftBreak(n) => n.span,
            Inline::Emphasis(n) => n.span,
            Inline::Strong(n) => n.span,
            Inline::Strikethrough(n) => n.span,
            Inline::CodeSpan(n) => n.span,
            Inline::Link(n) => n.span,
            Inline::Image(n) => n.span,
            Inline::Autolink(n) => n.span,
            Inline::AutolinkLiteral(n) => n.span,
            Inline::HtmlInline(n) => n.span,
            Inline::HardBreak(n) => n.span,
            Inline::FootnoteReference(n) => n.span,
            Inline::MathSpan(n) => n.span,
            Inline::WikiLink(n) => n.span,
            Inline::Liquid(n) => n.span,
            Inline::MdxExpression(n) => n.span,
            Inline::MdxJsx(n) => n.span,
        }
    }
}

#[derive(Debug)]
pub struct Paragraph<'a> {
    pub children: ArenaVec<'a, Inline<'a>>,
    /// Source lines that entered this paragraph as lazy continuations
    /// (unprefixed lines inside a blockquote/list).
    /// A printer must not merge or re-wrap across these: doing so changes meaning.
    pub lazy_lines: ArenaVec<'a, Span>,
    pub span: Span,
}

#[derive(Debug)]
pub struct Heading<'a> {
    /// 1–6 for ATX, 1–2 for setext.
    pub level: u8,
    pub kind: HeadingKind<'a>,
    pub children: ArenaVec<'a, Inline<'a>>,
    pub span: Span,
}

#[derive(Debug)]
pub enum HeadingKind<'a> {
    Atx,
    /// The underline is printed verbatim (its length is preserved).
    Setext {
        underline: Span,
        /// Source lines that entered the underlying paragraph as lazy continuations,
        /// as on [`Paragraph::lazy_lines`].
        /// A printer keeping the setext form must not merge or re-wrap across these.
        lazy_lines: ArenaVec<'a, Span>,
    },
}

#[derive(Clone, Copy, Debug)]
pub struct ThematicBreak {
    pub span: Span,
}

#[derive(Debug)]
pub struct CodeBlock<'a> {
    pub kind: CodeBlockKind,
    /// Logical content lines (container prefixes and fence/code indent stripped).
    /// The printer recomputes the minimum fence length from them.
    pub lines: ArenaVec<'a, Segment>,
    pub span: Span,
}

#[derive(Clone, Copy, Debug)]
pub enum CodeBlockKind {
    Fenced {
        /// `` ` `` or `~`.
        fence: u8,
        /// The info string's first word (mdast `lang`), if any.
        lang: Option<Span>,
        /// The rest of the info string after `lang` and the whitespace (mdast `meta`), if any.
        meta: Option<Span>,
    },
    /// Preserved as indented (never converted to fenced).
    Indented,
}

/// A raw HTML block. Contents are never parsed or formatted.
#[derive(Debug)]
pub struct HtmlBlock<'a> {
    /// CommonMark HTML block type 1–7.
    /// Types 1–5 run to their end marker (type 1 closing tag, `-->`, `?>`, `>`, `]]>`);
    /// types 6–7 end at a blank line,
    /// which is how markdown content interleaves between unbalanced tags.
    pub kind: u8,
    /// Logical content lines, verbatim.
    pub lines: ArenaVec<'a, Segment>,
    /// Whether the block's final line ending belongs to its content
    /// (micromark: `html.value` ends with a line ending).
    /// Only types 1–5, in two cases.
    /// The block was still open at EOF (or at a directive's closing fence), outside any blockquote.
    /// The line that closed its container opened a new one (list item, blockquote, footnote definition).
    /// Types 6–7 end at their last content line, so never.
    pub trailing_newline: bool,
    pub span: Span,
}

#[derive(Debug)]
pub struct Blockquote<'a> {
    pub children: ArenaVec<'a, Block<'a>>,
    pub span: Span,
}

#[derive(Debug)]
pub struct List<'a> {
    pub marker: ListMarker,
    /// Loose lists render children as paragraphs;
    /// blank-line placement around items decides this at parse time.
    pub tight: bool,
    pub children: ArenaVec<'a, ListItem<'a>>,
    pub span: Span,
}

#[derive(Clone, Copy, Debug)]
pub enum ListMarker {
    /// `-` / `*` / `+`.
    Bullet { marker: u8 },
    /// `.` / `)` after the number.
    Ordered { delimiter: u8 },
}

#[derive(Debug)]
pub struct ListItem<'a> {
    /// The marker itself: `-`, or the full `123.`.
    /// Ordered numbers stay raw
    /// (CommonMark caps them at 999,999,999; arithmetic on them overflows).
    pub marker: Span,
    /// Spaces between the marker and the content, for aligned-list judgment.
    pub padding: u8,
    /// GFM task list checkbox, when the item starts with one.
    /// The first paragraph begins after the checkbox and the whitespace before same-line content;
    /// when only whitespace follows on that line it stays in the paragraph,
    /// as the line's soft or hard break.
    pub checkbox: Option<TaskCheckbox>,
    /// A blank line separates two of this item's children (mdast's `listItem.spread`).
    /// Looseness is judged per item downstream (Prettier: this, or a blank line before the next item),
    /// while [`List::tight`] is the whole list's cmark verdict.
    pub spread: bool,
    pub children: ArenaVec<'a, Block<'a>>,
    pub span: Span,
}

/// `[ ]`, `[x]` or `[X]` (the raw span keeps the case).
#[derive(Clone, Copy, Debug)]
pub struct TaskCheckbox {
    pub span: Span,
    pub checked: bool,
}

/// A link reference definition: `[label]: dest "title"`.
///
/// Label and title may span lines,
/// and a multi-line span would include the container prefixes between them,
/// so both are logical-line pieces (see [`Segment::join`]).
/// Normalize the label from the joined pieces;
/// a title's continuation pieces keep their leading whitespace raw (micromark strips all of it).
#[derive(Debug)]
pub struct Definition<'a> {
    /// Between the brackets.
    pub label: ArenaVec<'a, Segment>,
    pub destination: Destination,
    /// Including the quotes.
    pub title: Option<ArenaVec<'a, Segment>>,
    pub span: Span,
}

#[derive(Debug)]
pub struct Table<'a> {
    /// One entry per column of the delimiter row.
    /// Alignments beyond the header row's cell count are dropped (GFM).
    pub align: ArenaVec<'a, TableAlign>,
    /// The first row is the header.
    pub children: ArenaVec<'a, TableRow<'a>>,
    pub span: Span,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TableAlign {
    None,
    Left,
    Center,
    Right,
}

#[derive(Debug)]
pub struct TableRow<'a> {
    pub children: ArenaVec<'a, TableCell<'a>>,
    pub span: Span,
}

#[derive(Debug)]
pub struct TableCell<'a> {
    pub children: ArenaVec<'a, Inline<'a>>,
    pub span: Span,
}

#[derive(Debug)]
pub struct FootnoteDefinition<'a> {
    pub label: Span,
    pub children: ArenaVec<'a, Block<'a>>,
    pub span: Span,
}

/// `$$ … $$` display math, content is kept raw.
/// As with code fences, the fence length is not stored
/// (printers normalize to `$$`; the `$` run at `span.start` gives it if ever needed).
#[derive(Debug)]
pub struct MathBlock<'a> {
    /// Raw text after the opening fence (`$$meta`), leading whitespace stripped.
    /// Never contains `$`.
    pub meta: Option<Span>,
    /// Logical content lines (container prefixes and the opening fence's indent stripped),
    /// fence lines excluded.
    pub lines: ArenaVec<'a, Segment>,
    pub span: Span,
}

/// A container directive:
///
/// ```markdown
/// :::note[label]{.class}
/// children
/// :::
/// ```
#[derive(Debug)]
pub struct ContainerDirective<'a> {
    /// The whole opening fence line, kept verbatim.
    /// Dialects disagree on its grammar (remark-directive vs markdown-it-container),
    /// so it is never normalized.
    pub opening: Span,
    /// The closing fence line, if the directive was explicitly closed.
    /// Nesting uses longer fences; as with code fences, the length is not stored
    /// (the `:` run at `opening.start` gives it).
    pub closing: Option<Span>,
    pub children: ArenaVec<'a, Block<'a>>,
    pub span: Span,
}

#[derive(Clone, Copy, Debug)]
pub struct Text {
    pub span: Span,
    /// Every byte is ASCII.
    /// Contains CJK characters.
    /// Classification (CJ vs K vs punctuation) is formatter policy and happens downstream.
    pub contains_cjk: bool,
}

#[derive(Debug)]
pub struct Emphasis<'a> {
    /// `*` or `_`. Normalization is context-dependent,
    /// so the source marker is a required input, not a derivable one.
    pub marker: u8,
    pub children: ArenaVec<'a, Inline<'a>>,
    pub span: Span,
}

#[derive(Debug)]
pub struct Strong<'a> {
    /// `*` or `_`.
    pub marker: u8,
    pub children: ArenaVec<'a, Inline<'a>>,
    pub span: Span,
}

/// GFM `~~x~~` (`~x~` too with `gfm_strikethrough_single_tilde`).
#[derive(Debug)]
pub struct Strikethrough<'a> {
    /// 1 or 2.
    pub tildes: u8,
    pub children: ArenaVec<'a, Inline<'a>>,
    pub span: Span,
}

#[derive(Debug)]
pub struct CodeSpan<'a> {
    /// Content between the backtick runs, one segment per line
    /// (a multi-line code span crosses container prefixes, so a single span can't cover it;
    /// a partially consumed tab leaves virtual spaces in `padding`).
    /// The printer recomputes the minimum backtick run length from these.
    /// Inside a table cell a `|` is `\|` in the source and stays so (nothing to escape).
    pub pieces: ArenaVec<'a, Segment>,
    pub span: Span,
}

/// `[text](dest "title")` or `[text][label]` / `[text][]` / `[text]`.
#[derive(Debug)]
pub struct Link<'a> {
    pub kind: LinkKind<'a>,
    pub children: ArenaVec<'a, Inline<'a>>,
    pub span: Span,
}

/// `![alt](dest "title")` and the reference forms.
#[derive(Debug)]
pub struct Image<'a> {
    pub kind: LinkKind<'a>,
    /// The raw text between `![` and `]`, as logical-line pieces
    /// (a printer reproduces it as written; mdast cooks it to `alt`).
    pub alt: ArenaVec<'a, Segment>,
    /// The alt text parsed as inline content.
    /// Nested link/image syntax is preserved here, not cooked.
    pub children: ArenaVec<'a, Inline<'a>>,
    pub span: Span,
}

/// Titles and labels are logical-line pieces for the same reason as on [`Definition`].
#[derive(Debug)]
pub enum LinkKind<'a> {
    Inline {
        destination: Destination,
        /// Including the quotes.
        title: Option<ArenaVec<'a, Segment>>,
    },
    Reference {
        kind: ReferenceKind,
        /// The text the reference resolves through:
        /// the second bracket's interior for `Full`, the link text for `Collapsed`/`Shortcut`.
        label: ArenaVec<'a, Segment>,
    },
}

/// Reference-link form.
/// Only `Full` is reformatted; the others are printed verbatim.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReferenceKind {
    /// `[text][label]`
    Full,
    /// `[text][]`
    Collapsed,
    /// `[text]`
    Shortcut,
}

/// A link/image destination, excluding the title.
#[derive(Clone, Copy, Debug)]
pub struct Destination {
    /// Covers the `<`/`>` when angle-bracketed.
    pub span: Span,
    /// `<dest>` form. Whether brackets are needed on output is a lexical judgment
    /// (spaces, `(`, `<`, `\` …), computed from the raw span.
    pub angle_bracketed: bool,
}

/// `<https://example.com>` / `<user@example.com>`.
#[derive(Clone, Copy, Debug)]
pub struct Autolink {
    pub span: Span,
    /// Email autolinks render with an implied `mailto:` that must not leak into output.
    pub email: bool,
}

/// GFM autolink literal (`www.example.com`, bare `https://…` / email).
#[derive(Clone, Copy, Debug)]
pub struct AutolinkLiteral {
    pub span: Span,
}

/// A raw inline HTML tag or comment.
/// The tag is a verbatim token; text around it is markdown.
#[derive(Debug)]
pub struct HtmlInline<'a> {
    /// Verbatim content, one segment per line, leading whitespace of continuation lines included
    /// (what mdast's `html.value` holds; only micromark's HTML compiler strips up to 3 columns of it).
    pub pieces: ArenaVec<'a, Segment>,
    pub span: Span,
}

#[derive(Clone, Copy, Debug)]
pub struct SoftBreak {
    /// Zero-width, at the first content byte of the following line
    /// (past the line ending and any container prefix).
    /// The line ending itself is not covered: it lies between the two sibling spans.
    pub span: Span,
}

#[derive(Clone, Copy, Debug)]
pub struct HardBreak {
    pub kind: HardBreakKind,
    pub span: Span,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HardBreakKind {
    /// Two-plus trailing spaces before the newline.
    Spaces,
    /// `\` before the newline.
    Backslash,
}

/// GFM `[^label]`.
#[derive(Clone, Copy, Debug)]
pub struct FootnoteReference {
    pub label: Span,
    pub span: Span,
}

/// `$ … $` inline math.
/// The whole construct, `$` sequences included, is kept raw;
/// content is never trimmed or decoded.
#[derive(Debug)]
pub struct MathSpan<'a> {
    /// Verbatim construct, one segment per line
    /// (a multi-line span crosses container prefixes, so a single span can't cover it).
    pub pieces: ArenaVec<'a, Segment>,
    pub span: Span,
}

/// `[[target]]`, alias-less.
/// Always single-line; the target is `span + 2 .. span - 2`, kept raw.
#[derive(Clone, Copy, Debug)]
pub struct WikiLink {
    pub span: Span,
}

/// A `{% … %}` / `{{ … }}` template tag, block-level or inline.
/// Never interpreted: the whole construct, delimiters included, is kept verbatim.
#[derive(Debug)]
pub struct Liquid<'a> {
    /// Verbatim construct, one segment per line (multi-line tags cross container prefixes).
    pub pieces: ArenaVec<'a, Segment>,
    pub span: Span,
}

/// An MDX `import`/`export` statement.
/// The parser only establishes the boundary (via the host's JS parser callback);
/// the content stays raw for embedded formatting.
#[derive(Clone, Copy, Debug)]
pub struct MdxEsm {
    pub span: Span,
}

/// An MDX `{expr}`.
/// Boundary only; the expression stays raw.
#[derive(Clone, Copy, Debug)]
pub struct MdxExpression {
    pub span: Span,
}

/// An MDX JSX element at flow level.
/// Tags stay raw for embedded formatting; children are markdown blocks.
#[derive(Debug)]
pub struct MdxJsxFlow<'a> {
    /// The opening (or self-closing) tag, raw.
    pub opening: Span,
    /// The closing tag, raw. `None` when self-closing.
    pub closing: Option<Span>,
    pub children: ArenaVec<'a, Block<'a>>,
    pub span: Span,
}

/// An MDX JSX element in text.
/// Children are inline markdown.
#[derive(Debug)]
pub struct MdxJsxText<'a> {
    pub opening: Span,
    pub closing: Option<Span>,
    pub children: ArenaVec<'a, Inline<'a>>,
    pub span: Span,
}