@markuplint/markdown-parser 5.0.0-rc.4 → 5.0.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,31 +25,20 @@ import { Parser, getNamespace } from '@markuplint/parser-utils';
25
25
  type MdastNode = RootContent;
26
26
 
27
27
  /**
28
- * Abstract base class for parsers that handle Markdown content.
29
- *
30
- * Provides shared logic for converting mdast nodes (headings, links, images,
31
- * lists, code, tables, etc.) into markuplint's AST. Both MarkdownParser and
32
- * MDXParser extend this class to avoid code duplication.
28
+ * Extends `Parser<MdastNode>` rather than `HtmlParser`: an earlier design
29
+ * extended `HtmlParser` and treated Markdown as opaque psblock nodes, which
30
+ * left Markdown constructs invisible to rules. Converting them to their HTML
31
+ * equivalents (with synthesized attributes such as `src`/`alt` from
32
+ * `![alt](src)`) is what makes Markdown content lintable.
33
33
  */
34
34
 
35
35
  export abstract class MarkdownAwareParser extends Parser<MdastNode> {
36
- /**
37
- * Stores link/image reference definitions (`[id]: url "title"`)
38
- * extracted during tokenization for resolving linkReference/imageReference nodes.
39
- */
40
36
  protected definitions = new Map<string, Definition>();
41
37
 
42
- /**
43
- * Offsets of table rows that are header rows (first row of each table).
44
- * Set by visitTableElement, read by nodeizeMarkdownNode for tableRow dispatch.
45
- */
38
+ /** Set by visitTableElement, read by nodeizeMarkdownNode for tableRow dispatch. */
46
39
  readonly #headerRowOffsets = new Set<number>();
47
40
 
48
- /**
49
- * Current cell element name ('th' or 'td').
50
- * Set by tableRow processing, read by tableCell processing.
51
- * Reset to 'td' after each row.
52
- */
41
+ /** Set by tableRow processing, read by tableCell processing; reset to 'td' after each row. */
53
42
  #currentCellName: 'th' | 'td' = 'td';
54
43
 
55
44
  constructor(options?: ParserOptions) {
@@ -57,8 +46,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
57
46
  }
58
47
 
59
48
  /**
60
- * Resets mutable state accumulated during a previous `parse()` call.
61
- *
62
49
  * Must be called at the beginning of every `tokenize()` invocation to
63
50
  * prevent definitions, header-row offsets, and cell-name state from
64
51
  * leaking across successive `parse()` calls on the same parser instance.
@@ -70,13 +57,8 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
70
57
  }
71
58
 
72
59
  /**
73
- * Adjusts the flattened node list for Markdown output.
74
- *
75
60
  * Disables whitespace and invalid-node exposure because Markdown
76
61
  * generates only synthetic elements with no real HTML whitespace tokens.
77
- *
78
- * @param nodeList - The flattened node tree produced by the base class.
79
- * @returns The adjusted node list.
80
62
  */
81
63
  afterFlattenNodes(nodeList: readonly MLASTNodeTreeItem[]) {
82
64
  return super.afterFlattenNodes(nodeList, {
@@ -86,15 +68,8 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
86
68
  }
87
69
 
88
70
  /**
89
- * Creates a synthetic HTML attribute token for Markdown-derived elements.
90
- *
91
71
  * The attribute positions point to the element's own token range because
92
72
  * Markdown syntax does not have discrete attribute source positions.
93
- *
94
- * @param name - The attribute name (e.g., `"href"`, `"alt"`).
95
- * @param value - The attribute value extracted from Markdown syntax.
96
- * @param token - The source token whose position is reused for the attribute.
97
- * @returns A fully-formed HTML attribute node.
98
73
  */
99
74
  protected createSyntheticAttr(name: string, value: string, token: Token): MLASTHTMLAttr {
100
75
  const emptyToken = this.createToken('', token.offset, token.line, token.col);
@@ -118,17 +93,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
118
93
  };
119
94
  }
120
95
 
121
- /**
122
- * Builds a generic HTML element node from a Markdown construct.
123
- *
124
- * @param token - The source token covering the entire construct.
125
- * @param nodeName - The HTML element name (e.g., `"p"`, `"h1"`, `"li"`).
126
- * @param childNodes - The mdast children to recurse into.
127
- * @param depth - Current nesting depth in the AST.
128
- * @param parentNode - Parent AST node, or `null` for top-level nodes.
129
- * @param attributes - Optional pre-built attributes to attach.
130
- * @returns The element node followed by its descendants.
131
- */
132
96
  protected visitMarkdownElement(
133
97
  token: Token,
134
98
  nodeName: string,
@@ -165,15 +129,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
165
129
  return [startTag, ...siblings];
166
130
  }
167
131
 
168
- /**
169
- * Builds an `<a>` element with `href` (and optionally `title`) attributes.
170
- *
171
- * @param originNode - The mdast `link` node.
172
- * @param token - The source token covering the link.
173
- * @param depth - Current nesting depth.
174
- * @param parentNode - Parent AST node, or `null` for top-level.
175
- * @returns The `<a>` element node and its descendants.
176
- */
177
132
  protected visitLinkElement(
178
133
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
179
134
  originNode: Link,
@@ -190,15 +145,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
190
145
  return this.visitMarkdownElement(token, 'a', originNode.children, depth, parentNode, attrs);
191
146
  }
192
147
 
193
- /**
194
- * Builds an `<img>` element with `src`, `alt`, and optionally `title` attributes.
195
- *
196
- * @param originNode - The mdast `image` node.
197
- * @param token - The source token covering the image.
198
- * @param depth - Current nesting depth.
199
- * @param parentNode - Parent AST node, or `null` for top-level.
200
- * @returns The `<img>` element node.
201
- */
202
148
  protected visitImageElement(
203
149
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
204
150
  originNode: Image,
@@ -218,16 +164,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
218
164
  return this.visitMarkdownElement(token, 'img', [], depth, parentNode, attrs);
219
165
  }
220
166
 
221
- /**
222
- * Builds a `<ul>` or `<ol>` element. Adds a `start` attribute when the
223
- * ordered list begins at a number other than 1.
224
- *
225
- * @param originNode - The mdast `list` node.
226
- * @param token - The source token covering the list.
227
- * @param depth - Current nesting depth.
228
- * @param parentNode - Parent AST node, or `null` for top-level.
229
- * @returns The list element node and its descendants.
230
- */
231
167
  protected visitListElement(
232
168
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
233
169
  originNode: List,
@@ -245,17 +181,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
245
181
  return this.visitMarkdownElement(token, nodeName, originNode.children, depth, parentNode, attrs);
246
182
  }
247
183
 
248
- /**
249
- * Builds a `<code>` element for inline code spans (backtick-delimited).
250
- *
251
- * @param originNode - The mdast `inlineCode` node.
252
- * @param token - The source token covering the code span.
253
- * @param offset - Start offset in the original source.
254
- * @param endOffset - End offset in the original source.
255
- * @param depth - Current nesting depth.
256
- * @param parentNode - Parent AST node, or `null` for top-level.
257
- * @returns The `<code>` element node (with a text child when content is found).
258
- */
259
184
  protected visitInlineCode(
260
185
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
261
186
  originNode: InlineCode,
@@ -312,16 +237,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
312
237
  return [startTag];
313
238
  }
314
239
 
315
- /**
316
- * Builds a `<pre><code>` structure for fenced code blocks.
317
- * When a language is specified, adds `class="language-{lang}"` to the `<code>` element.
318
- *
319
- * @param originNode - The mdast `code` node.
320
- * @param token - The source token covering the fenced block.
321
- * @param depth - Current nesting depth.
322
- * @param parentNode - Parent AST node, or `null` for top-level.
323
- * @returns The `<pre>` and `<code>` element nodes.
324
- */
325
240
  protected visitCodeBlock(
326
241
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
327
242
  originNode: Code,
@@ -329,7 +244,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
329
244
  depth: number,
330
245
  parentNode: MLASTParentNode | null,
331
246
  ): readonly MLASTNodeTreeItem[] {
332
- // Build <pre> element
333
247
  const preTag: MLASTElement = {
334
248
  ...token,
335
249
  ...this.createToken(token),
@@ -351,7 +265,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
351
265
  nodeName: 'pre',
352
266
  };
353
267
 
354
- // Build <code> element as child of <pre>
355
268
  const codeAttrs: MLASTHTMLAttr[] = [];
356
269
  if (originNode.lang) {
357
270
  codeAttrs.push(this.createSyntheticAttr('class', `language-${originNode.lang}`, token));
@@ -378,7 +291,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
378
291
  nodeName: 'code',
379
292
  };
380
293
 
381
- // Add code content as text node if present
382
294
  if (originNode.value.length > 0) {
383
295
  const position = originNode.position;
384
296
  if (position) {
@@ -409,16 +321,6 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
409
321
  return [preTag, codeTag];
410
322
  }
411
323
 
412
- /**
413
- * Builds a `<table>` element from a GFM table node.
414
- * Marks the first row's offset as a header row so that its cells become `<th>`.
415
- *
416
- * @param originNode - The mdast `table` node (GFM extension).
417
- * @param token - The source token covering the table.
418
- * @param depth - Current nesting depth.
419
- * @param parentNode - Parent AST node, or `null` for top-level.
420
- * @returns The `<table>` element node and its descendants.
421
- */
422
324
  protected visitTableElement(
423
325
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
424
326
  originNode: Table,
@@ -435,18 +337,9 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
435
337
  }
436
338
 
437
339
  /**
438
- * Dispatches a single mdast node to the appropriate visit method.
439
- *
440
- * @param originNode - The mdast node to convert.
441
- * @param token - The source token covering the node's range.
442
- * @param offset - Start offset in the original source.
443
- * @param endOffset - End offset in the original source.
444
- * @param depth - Current nesting depth.
445
- * @param parentNode - Parent AST node, or `null` for top-level nodes.
446
- * @returns An array of AST nodes for recognized Markdown constructs,
447
- * or `null` when the node type is not handled here (the caller is
448
- * responsible for handling it — typically `text`, `html`, or
449
- * parser-specific node types).
340
+ * Returns `null` when the node type is not handled here, signalling that
341
+ * the caller must handle it (typically `text`, `html`, or parser-specific
342
+ * node types).
450
343
  */
451
344
  protected nodeizeMarkdownNode(
452
345
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -484,6 +377,8 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
484
377
  return this.visitMarkdownElement(token, 'li', originNode.children, depth, parentNode);
485
378
  }
486
379
  case 'blockquote': {
380
+ // Markdown's `> quote` syntax has no equivalent of the HTML
381
+ // `cite` attribute, so no `cite` attribute is synthesized.
487
382
  return this.visitMarkdownElement(token, 'blockquote', originNode.children, depth, parentNode);
488
383
  }
489
384
  case 'thematicBreak': {
@@ -559,8 +454,9 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
559
454
  }
560
455
 
561
456
  /**
562
- * Resolves a linkReference using collected definitions, producing an `<a>` element.
563
- * Falls back to a psblock when the definition is not found.
457
+ * Note: remark-parse resolves references at parse time when definitions
458
+ * exist, so unresolved references typically appear as plain text rather
459
+ * than `linkReference` nodes; the psblock fallback is a defensive path.
564
460
  */
565
461
  #visitLinkReference(
566
462
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -589,8 +485,8 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
589
485
  }
590
486
 
591
487
  /**
592
- * Resolves an imageReference using collected definitions, producing an `<img>` element.
593
- * Falls back to a psblock when the definition is not found.
488
+ * Note: as with linkReference, unresolved references typically appear as
489
+ * plain text in the mdast, so the psblock fallback is a defensive path.
594
490
  */
595
491
  #visitImageReference(
596
492
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -622,13 +518,9 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
622
518
  }
623
519
 
624
520
  /**
625
- * Extracts definition nodes from mdast children and populates `this.definitions`.
626
- *
627
521
  * Per CommonMark spec, the first definition for a given identifier takes
628
522
  * precedence. remark-parse emits all definition nodes in source order, so
629
523
  * we skip duplicates via `Map.has` to honour the first-wins rule.
630
- *
631
- * @param children - The root-level mdast children to scan for `definition` nodes.
632
524
  */
633
525
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
634
526
  protected collectDefinitions(children: readonly RootContent[]) {
@@ -641,15 +533,9 @@ export abstract class MarkdownAwareParser extends Parser<MdastNode> {
641
533
  }
642
534
 
643
535
  /**
644
- * Computes the 1-based line number and 1-based column for a given offset.
645
- *
646
536
  * Equivalent to `getPosition()` in `@markuplint/parser-utils`, but that
647
537
  * function is not exported from the package. Kept as a standalone utility
648
538
  * to avoid coupling to parser-utils internals.
649
- *
650
- * @param source - The full source string.
651
- * @param offset - The 0-based character offset to resolve.
652
- * @returns An object with 1-based `line` and `col` values.
653
539
  */
654
540
  export function getLineAndColumn(source: string, offset: number): { line: number; col: number } {
655
541
  let line = 1;
package/src/parser.ts CHANGED
@@ -17,6 +17,12 @@ type MdastNode = RootContent;
17
17
  * Uses remark-parse to produce an mdast, then maps Markdown constructs
18
18
  * (headings, paragraphs, lists, links, etc.) to their corresponding HTML
19
19
  * element AST nodes. Raw HTML regions are parsed via HtmlParser.
20
+ *
21
+ * remark-parse (the unified ecosystem's de facto standard) was chosen over
22
+ * `@mdx-js/mdx`, which wraps remark-parse internally and adds about 24
23
+ * unnecessary dependencies for JS compilation that linting does not need,
24
+ * and over driving the lower-level tokenizer directly, which would require
25
+ * hand-building the mdast conversion with no practical benefit.
20
26
  */
21
27
  class MarkdownParser extends MarkdownAwareParser {
22
28
  readonly #htmlParser = new HtmlParser();
@@ -101,7 +107,20 @@ class MarkdownParser extends MarkdownAwareParser {
101
107
  offsetOffset: offset,
102
108
  offsetLine: line,
103
109
  offsetColumn: col,
110
+ // HTML embedded inside Markdown is always a partial — never a
111
+ // full document — so force fragment parsing to keep parse5 from
112
+ // emitting `missing-doctype` / `misplaced-doctype` on every
113
+ // inline HTML block. Users cannot meaningfully override this
114
+ // because there is no Markdown construct that wraps a complete
115
+ // HTML document.
116
+ documentMode: 'fragment',
104
117
  });
118
+ // Surface tokenizer-level parse errors (e.g. `duplicate-attribute`)
119
+ // collected by the embedded HtmlParser. Without this, every parse
120
+ // error inside an inline HTML block would be silently dropped on
121
+ // the way back from `#htmlParser.parse()` even though the user has
122
+ // opted in via `severity.parseError`.
123
+ this.accumulateParseErrors(doc.parseErrors);
105
124
  return [...doc.nodeList];
106
125
  }
107
126
  }