@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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-rc.5](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.4...v5.0.0-rc.5) (2026-08-28)
7
+
8
+ ### Features
9
+
10
+ - **markdown-parser,pug-parser:** force fragment parsing for embedded HTML ([57c4172](https://github.com/markuplint/markuplint/commit/57c4172d5a68adb7e20a4f4e4d8dbe802983e3ca)), closes [#3844](https://github.com/markuplint/markuplint/issues/3844)
11
+ - **markdown-parser,pug-parser:** forward embedded HTML parseErrors to outer document ([9ec7988](https://github.com/markuplint/markuplint/commit/9ec79882840cbedd76c5b8f774ff10c876d2c858)), closes [#3844](https://github.com/markuplint/markuplint/issues/3844)
12
+
6
13
  # [5.0.0-rc.4](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.3...v5.0.0-rc.4) (2026-04-19)
7
14
 
8
15
  **Note:** Version bump only for package @markuplint/markdown-parser
@@ -4,163 +4,56 @@ import type { Code, Definition, Image, InlineCode, Link, List, RootContent, Tabl
4
4
  import { Parser } from '@markuplint/parser-utils';
5
5
  type MdastNode = RootContent;
6
6
  /**
7
- * Abstract base class for parsers that handle Markdown content.
8
- *
9
- * Provides shared logic for converting mdast nodes (headings, links, images,
10
- * lists, code, tables, etc.) into markuplint's AST. Both MarkdownParser and
11
- * MDXParser extend this class to avoid code duplication.
7
+ * Extends `Parser<MdastNode>` rather than `HtmlParser`: an earlier design
8
+ * extended `HtmlParser` and treated Markdown as opaque psblock nodes, which
9
+ * left Markdown constructs invisible to rules. Converting them to their HTML
10
+ * equivalents (with synthesized attributes such as `src`/`alt` from
11
+ * `![alt](src)`) is what makes Markdown content lintable.
12
12
  */
13
13
  export declare abstract class MarkdownAwareParser extends Parser<MdastNode> {
14
14
  #private;
15
- /**
16
- * Stores link/image reference definitions (`[id]: url "title"`)
17
- * extracted during tokenization for resolving linkReference/imageReference nodes.
18
- */
19
15
  protected definitions: Map<string, Definition>;
20
16
  constructor(options?: ParserOptions);
21
17
  /**
22
- * Resets mutable state accumulated during a previous `parse()` call.
23
- *
24
18
  * Must be called at the beginning of every `tokenize()` invocation to
25
19
  * prevent definitions, header-row offsets, and cell-name state from
26
20
  * leaking across successive `parse()` calls on the same parser instance.
27
21
  */
28
22
  protected resetMarkdownState(): void;
29
23
  /**
30
- * Adjusts the flattened node list for Markdown output.
31
- *
32
24
  * Disables whitespace and invalid-node exposure because Markdown
33
25
  * generates only synthetic elements with no real HTML whitespace tokens.
34
- *
35
- * @param nodeList - The flattened node tree produced by the base class.
36
- * @returns The adjusted node list.
37
26
  */
38
27
  afterFlattenNodes(nodeList: readonly MLASTNodeTreeItem[]): readonly MLASTNodeTreeItem[];
39
28
  /**
40
- * Creates a synthetic HTML attribute token for Markdown-derived elements.
41
- *
42
29
  * The attribute positions point to the element's own token range because
43
30
  * Markdown syntax does not have discrete attribute source positions.
44
- *
45
- * @param name - The attribute name (e.g., `"href"`, `"alt"`).
46
- * @param value - The attribute value extracted from Markdown syntax.
47
- * @param token - The source token whose position is reused for the attribute.
48
- * @returns A fully-formed HTML attribute node.
49
31
  */
50
32
  protected createSyntheticAttr(name: string, value: string, token: Token): MLASTHTMLAttr;
51
- /**
52
- * Builds a generic HTML element node from a Markdown construct.
53
- *
54
- * @param token - The source token covering the entire construct.
55
- * @param nodeName - The HTML element name (e.g., `"p"`, `"h1"`, `"li"`).
56
- * @param childNodes - The mdast children to recurse into.
57
- * @param depth - Current nesting depth in the AST.
58
- * @param parentNode - Parent AST node, or `null` for top-level nodes.
59
- * @param attributes - Optional pre-built attributes to attach.
60
- * @returns The element node followed by its descendants.
61
- */
62
33
  protected visitMarkdownElement(token: Token, nodeName: string, childNodes: readonly MdastNode[], depth: number, parentNode: MLASTParentNode | null, attributes?: readonly MLASTAttr[]): readonly MLASTNodeTreeItem[];
63
- /**
64
- * Builds an `<a>` element with `href` (and optionally `title`) attributes.
65
- *
66
- * @param originNode - The mdast `link` node.
67
- * @param token - The source token covering the link.
68
- * @param depth - Current nesting depth.
69
- * @param parentNode - Parent AST node, or `null` for top-level.
70
- * @returns The `<a>` element node and its descendants.
71
- */
72
34
  protected visitLinkElement(originNode: Link, token: Token, depth: number, parentNode: MLASTParentNode | null): readonly MLASTNodeTreeItem[];
73
- /**
74
- * Builds an `<img>` element with `src`, `alt`, and optionally `title` attributes.
75
- *
76
- * @param originNode - The mdast `image` node.
77
- * @param token - The source token covering the image.
78
- * @param depth - Current nesting depth.
79
- * @param parentNode - Parent AST node, or `null` for top-level.
80
- * @returns The `<img>` element node.
81
- */
82
35
  protected visitImageElement(originNode: Image, token: Token, depth: number, parentNode: MLASTParentNode | null): readonly MLASTNodeTreeItem[];
83
- /**
84
- * Builds a `<ul>` or `<ol>` element. Adds a `start` attribute when the
85
- * ordered list begins at a number other than 1.
86
- *
87
- * @param originNode - The mdast `list` node.
88
- * @param token - The source token covering the list.
89
- * @param depth - Current nesting depth.
90
- * @param parentNode - Parent AST node, or `null` for top-level.
91
- * @returns The list element node and its descendants.
92
- */
93
36
  protected visitListElement(originNode: List, token: Token, depth: number, parentNode: MLASTParentNode | null): readonly MLASTNodeTreeItem[];
94
- /**
95
- * Builds a `<code>` element for inline code spans (backtick-delimited).
96
- *
97
- * @param originNode - The mdast `inlineCode` node.
98
- * @param token - The source token covering the code span.
99
- * @param offset - Start offset in the original source.
100
- * @param endOffset - End offset in the original source.
101
- * @param depth - Current nesting depth.
102
- * @param parentNode - Parent AST node, or `null` for top-level.
103
- * @returns The `<code>` element node (with a text child when content is found).
104
- */
105
37
  protected visitInlineCode(originNode: InlineCode, token: Token, offset: number, endOffset: number, depth: number, parentNode: MLASTParentNode | null): readonly MLASTNodeTreeItem[];
106
- /**
107
- * Builds a `<pre><code>` structure for fenced code blocks.
108
- * When a language is specified, adds `class="language-{lang}"` to the `<code>` element.
109
- *
110
- * @param originNode - The mdast `code` node.
111
- * @param token - The source token covering the fenced block.
112
- * @param depth - Current nesting depth.
113
- * @param parentNode - Parent AST node, or `null` for top-level.
114
- * @returns The `<pre>` and `<code>` element nodes.
115
- */
116
38
  protected visitCodeBlock(originNode: Code, token: Token, depth: number, parentNode: MLASTParentNode | null): readonly MLASTNodeTreeItem[];
117
- /**
118
- * Builds a `<table>` element from a GFM table node.
119
- * Marks the first row's offset as a header row so that its cells become `<th>`.
120
- *
121
- * @param originNode - The mdast `table` node (GFM extension).
122
- * @param token - The source token covering the table.
123
- * @param depth - Current nesting depth.
124
- * @param parentNode - Parent AST node, or `null` for top-level.
125
- * @returns The `<table>` element node and its descendants.
126
- */
127
39
  protected visitTableElement(originNode: Table, token: Token, depth: number, parentNode: MLASTParentNode | null): readonly MLASTNodeTreeItem[];
128
40
  /**
129
- * Dispatches a single mdast node to the appropriate visit method.
130
- *
131
- * @param originNode - The mdast node to convert.
132
- * @param token - The source token covering the node's range.
133
- * @param offset - Start offset in the original source.
134
- * @param endOffset - End offset in the original source.
135
- * @param depth - Current nesting depth.
136
- * @param parentNode - Parent AST node, or `null` for top-level nodes.
137
- * @returns An array of AST nodes for recognized Markdown constructs,
138
- * or `null` when the node type is not handled here (the caller is
139
- * responsible for handling it — typically `text`, `html`, or
140
- * parser-specific node types).
41
+ * Returns `null` when the node type is not handled here, signalling that
42
+ * the caller must handle it (typically `text`, `html`, or parser-specific
43
+ * node types).
141
44
  */
142
45
  protected nodeizeMarkdownNode(originNode: MdastNode, token: Token, offset: number, endOffset: number, depth: number, parentNode: MLASTParentNode | null): readonly MLASTNodeTreeItem[] | null;
143
46
  /**
144
- * Extracts definition nodes from mdast children and populates `this.definitions`.
145
- *
146
47
  * Per CommonMark spec, the first definition for a given identifier takes
147
48
  * precedence. remark-parse emits all definition nodes in source order, so
148
49
  * we skip duplicates via `Map.has` to honour the first-wins rule.
149
- *
150
- * @param children - The root-level mdast children to scan for `definition` nodes.
151
50
  */
152
51
  protected collectDefinitions(children: readonly RootContent[]): void;
153
52
  }
154
53
  /**
155
- * Computes the 1-based line number and 1-based column for a given offset.
156
- *
157
54
  * Equivalent to `getPosition()` in `@markuplint/parser-utils`, but that
158
55
  * function is not exported from the package. Kept as a standalone utility
159
56
  * to avoid coupling to parser-utils internals.
160
- *
161
- * @param source - The full source string.
162
- * @param offset - The 0-based character offset to resolve.
163
- * @returns An object with 1-based `line` and `col` values.
164
57
  */
165
58
  export declare function getLineAndColumn(source: string, offset: number): {
166
59
  line: number;
@@ -1,34 +1,21 @@
1
1
  import { Parser, getNamespace } from '@markuplint/parser-utils';
2
2
  /**
3
- * Abstract base class for parsers that handle Markdown content.
4
- *
5
- * Provides shared logic for converting mdast nodes (headings, links, images,
6
- * lists, code, tables, etc.) into markuplint's AST. Both MarkdownParser and
7
- * MDXParser extend this class to avoid code duplication.
3
+ * Extends `Parser<MdastNode>` rather than `HtmlParser`: an earlier design
4
+ * extended `HtmlParser` and treated Markdown as opaque psblock nodes, which
5
+ * left Markdown constructs invisible to rules. Converting them to their HTML
6
+ * equivalents (with synthesized attributes such as `src`/`alt` from
7
+ * `![alt](src)`) is what makes Markdown content lintable.
8
8
  */
9
9
  export class MarkdownAwareParser extends Parser {
10
- /**
11
- * Stores link/image reference definitions (`[id]: url "title"`)
12
- * extracted during tokenization for resolving linkReference/imageReference nodes.
13
- */
14
10
  definitions = new Map();
15
- /**
16
- * Offsets of table rows that are header rows (first row of each table).
17
- * Set by visitTableElement, read by nodeizeMarkdownNode for tableRow dispatch.
18
- */
11
+ /** Set by visitTableElement, read by nodeizeMarkdownNode for tableRow dispatch. */
19
12
  #headerRowOffsets = new Set();
20
- /**
21
- * Current cell element name ('th' or 'td').
22
- * Set by tableRow processing, read by tableCell processing.
23
- * Reset to 'td' after each row.
24
- */
13
+ /** Set by tableRow processing, read by tableCell processing; reset to 'td' after each row. */
25
14
  #currentCellName = 'td';
26
15
  constructor(options) {
27
16
  super(options);
28
17
  }
29
18
  /**
30
- * Resets mutable state accumulated during a previous `parse()` call.
31
- *
32
19
  * Must be called at the beginning of every `tokenize()` invocation to
33
20
  * prevent definitions, header-row offsets, and cell-name state from
34
21
  * leaking across successive `parse()` calls on the same parser instance.
@@ -39,13 +26,8 @@ export class MarkdownAwareParser extends Parser {
39
26
  this.#currentCellName = 'td';
40
27
  }
41
28
  /**
42
- * Adjusts the flattened node list for Markdown output.
43
- *
44
29
  * Disables whitespace and invalid-node exposure because Markdown
45
30
  * generates only synthetic elements with no real HTML whitespace tokens.
46
- *
47
- * @param nodeList - The flattened node tree produced by the base class.
48
- * @returns The adjusted node list.
49
31
  */
50
32
  afterFlattenNodes(nodeList) {
51
33
  return super.afterFlattenNodes(nodeList, {
@@ -54,15 +36,8 @@ export class MarkdownAwareParser extends Parser {
54
36
  });
55
37
  }
56
38
  /**
57
- * Creates a synthetic HTML attribute token for Markdown-derived elements.
58
- *
59
39
  * The attribute positions point to the element's own token range because
60
40
  * Markdown syntax does not have discrete attribute source positions.
61
- *
62
- * @param name - The attribute name (e.g., `"href"`, `"alt"`).
63
- * @param value - The attribute value extracted from Markdown syntax.
64
- * @param token - The source token whose position is reused for the attribute.
65
- * @returns A fully-formed HTML attribute node.
66
41
  */
67
42
  createSyntheticAttr(name, value, token) {
68
43
  const emptyToken = this.createToken('', token.offset, token.line, token.col);
@@ -84,17 +59,6 @@ export class MarkdownAwareParser extends Parser {
84
59
  isDuplicatable: false,
85
60
  };
86
61
  }
87
- /**
88
- * Builds a generic HTML element node from a Markdown construct.
89
- *
90
- * @param token - The source token covering the entire construct.
91
- * @param nodeName - The HTML element name (e.g., `"p"`, `"h1"`, `"li"`).
92
- * @param childNodes - The mdast children to recurse into.
93
- * @param depth - Current nesting depth in the AST.
94
- * @param parentNode - Parent AST node, or `null` for top-level nodes.
95
- * @param attributes - Optional pre-built attributes to attach.
96
- * @returns The element node followed by its descendants.
97
- */
98
62
  visitMarkdownElement(token, nodeName,
99
63
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
100
64
  childNodes, depth, parentNode, attributes = []) {
@@ -122,15 +86,6 @@ export class MarkdownAwareParser extends Parser {
122
86
  const siblings = this.visitChildren([...childNodes], startTag);
123
87
  return [startTag, ...siblings];
124
88
  }
125
- /**
126
- * Builds an `<a>` element with `href` (and optionally `title`) attributes.
127
- *
128
- * @param originNode - The mdast `link` node.
129
- * @param token - The source token covering the link.
130
- * @param depth - Current nesting depth.
131
- * @param parentNode - Parent AST node, or `null` for top-level.
132
- * @returns The `<a>` element node and its descendants.
133
- */
134
89
  visitLinkElement(
135
90
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
136
91
  originNode, token, depth, parentNode) {
@@ -140,15 +95,6 @@ export class MarkdownAwareParser extends Parser {
140
95
  }
141
96
  return this.visitMarkdownElement(token, 'a', originNode.children, depth, parentNode, attrs);
142
97
  }
143
- /**
144
- * Builds an `<img>` element with `src`, `alt`, and optionally `title` attributes.
145
- *
146
- * @param originNode - The mdast `image` node.
147
- * @param token - The source token covering the image.
148
- * @param depth - Current nesting depth.
149
- * @param parentNode - Parent AST node, or `null` for top-level.
150
- * @returns The `<img>` element node.
151
- */
152
98
  visitImageElement(
153
99
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
154
100
  originNode, token, depth, parentNode) {
@@ -161,16 +107,6 @@ export class MarkdownAwareParser extends Parser {
161
107
  }
162
108
  return this.visitMarkdownElement(token, 'img', [], depth, parentNode, attrs);
163
109
  }
164
- /**
165
- * Builds a `<ul>` or `<ol>` element. Adds a `start` attribute when the
166
- * ordered list begins at a number other than 1.
167
- *
168
- * @param originNode - The mdast `list` node.
169
- * @param token - The source token covering the list.
170
- * @param depth - Current nesting depth.
171
- * @param parentNode - Parent AST node, or `null` for top-level.
172
- * @returns The list element node and its descendants.
173
- */
174
110
  visitListElement(
175
111
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
176
112
  originNode, token, depth, parentNode) {
@@ -181,17 +117,6 @@ export class MarkdownAwareParser extends Parser {
181
117
  }
182
118
  return this.visitMarkdownElement(token, nodeName, originNode.children, depth, parentNode, attrs);
183
119
  }
184
- /**
185
- * Builds a `<code>` element for inline code spans (backtick-delimited).
186
- *
187
- * @param originNode - The mdast `inlineCode` node.
188
- * @param token - The source token covering the code span.
189
- * @param offset - Start offset in the original source.
190
- * @param endOffset - End offset in the original source.
191
- * @param depth - Current nesting depth.
192
- * @param parentNode - Parent AST node, or `null` for top-level.
193
- * @returns The `<code>` element node (with a text child when content is found).
194
- */
195
120
  visitInlineCode(
196
121
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
197
122
  originNode, token, offset, endOffset, depth, parentNode) {
@@ -236,20 +161,9 @@ export class MarkdownAwareParser extends Parser {
236
161
  this.appendChild(startTag, textNode);
237
162
  return [startTag];
238
163
  }
239
- /**
240
- * Builds a `<pre><code>` structure for fenced code blocks.
241
- * When a language is specified, adds `class="language-{lang}"` to the `<code>` element.
242
- *
243
- * @param originNode - The mdast `code` node.
244
- * @param token - The source token covering the fenced block.
245
- * @param depth - Current nesting depth.
246
- * @param parentNode - Parent AST node, or `null` for top-level.
247
- * @returns The `<pre>` and `<code>` element nodes.
248
- */
249
164
  visitCodeBlock(
250
165
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
251
166
  originNode, token, depth, parentNode) {
252
- // Build <pre> element
253
167
  const preTag = {
254
168
  ...token,
255
169
  ...this.createToken(token),
@@ -270,7 +184,6 @@ export class MarkdownAwareParser extends Parser {
270
184
  isFragment: false,
271
185
  nodeName: 'pre',
272
186
  };
273
- // Build <code> element as child of <pre>
274
187
  const codeAttrs = [];
275
188
  if (originNode.lang) {
276
189
  codeAttrs.push(this.createSyntheticAttr('class', `language-${originNode.lang}`, token));
@@ -295,7 +208,6 @@ export class MarkdownAwareParser extends Parser {
295
208
  isFragment: false,
296
209
  nodeName: 'code',
297
210
  };
298
- // Add code content as text node if present
299
211
  if (originNode.value.length > 0) {
300
212
  const position = originNode.position;
301
213
  if (position) {
@@ -321,16 +233,6 @@ export class MarkdownAwareParser extends Parser {
321
233
  this.appendChild(preTag, codeTag);
322
234
  return [preTag, codeTag];
323
235
  }
324
- /**
325
- * Builds a `<table>` element from a GFM table node.
326
- * Marks the first row's offset as a header row so that its cells become `<th>`.
327
- *
328
- * @param originNode - The mdast `table` node (GFM extension).
329
- * @param token - The source token covering the table.
330
- * @param depth - Current nesting depth.
331
- * @param parentNode - Parent AST node, or `null` for top-level.
332
- * @returns The `<table>` element node and its descendants.
333
- */
334
236
  visitTableElement(
335
237
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
336
238
  originNode, token, depth, parentNode) {
@@ -341,18 +243,9 @@ export class MarkdownAwareParser extends Parser {
341
243
  return this.visitMarkdownElement(token, 'table', originNode.children, depth, parentNode);
342
244
  }
343
245
  /**
344
- * Dispatches a single mdast node to the appropriate visit method.
345
- *
346
- * @param originNode - The mdast node to convert.
347
- * @param token - The source token covering the node's range.
348
- * @param offset - Start offset in the original source.
349
- * @param endOffset - End offset in the original source.
350
- * @param depth - Current nesting depth.
351
- * @param parentNode - Parent AST node, or `null` for top-level nodes.
352
- * @returns An array of AST nodes for recognized Markdown constructs,
353
- * or `null` when the node type is not handled here (the caller is
354
- * responsible for handling it — typically `text`, `html`, or
355
- * parser-specific node types).
246
+ * Returns `null` when the node type is not handled here, signalling that
247
+ * the caller must handle it (typically `text`, `html`, or parser-specific
248
+ * node types).
356
249
  */
357
250
  nodeizeMarkdownNode(
358
251
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -384,6 +277,8 @@ export class MarkdownAwareParser extends Parser {
384
277
  return this.visitMarkdownElement(token, 'li', originNode.children, depth, parentNode);
385
278
  }
386
279
  case 'blockquote': {
280
+ // Markdown's `> quote` syntax has no equivalent of the HTML
281
+ // `cite` attribute, so no `cite` attribute is synthesized.
387
282
  return this.visitMarkdownElement(token, 'blockquote', originNode.children, depth, parentNode);
388
283
  }
389
284
  case 'thematicBreak': {
@@ -446,8 +341,9 @@ export class MarkdownAwareParser extends Parser {
446
341
  }
447
342
  }
448
343
  /**
449
- * Resolves a linkReference using collected definitions, producing an `<a>` element.
450
- * Falls back to a psblock when the definition is not found.
344
+ * Note: remark-parse resolves references at parse time when definitions
345
+ * exist, so unresolved references typically appear as plain text rather
346
+ * than `linkReference` nodes; the psblock fallback is a defensive path.
451
347
  */
452
348
  #visitLinkReference(
453
349
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -469,8 +365,8 @@ export class MarkdownAwareParser extends Parser {
469
365
  return this.visitMarkdownElement(token, 'a', originNode.children, depth, parentNode, attrs);
470
366
  }
471
367
  /**
472
- * Resolves an imageReference using collected definitions, producing an `<img>` element.
473
- * Falls back to a psblock when the definition is not found.
368
+ * Note: as with linkReference, unresolved references typically appear as
369
+ * plain text in the mdast, so the psblock fallback is a defensive path.
474
370
  */
475
371
  #visitImageReference(
476
372
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -495,13 +391,9 @@ export class MarkdownAwareParser extends Parser {
495
391
  return this.visitMarkdownElement(token, 'img', [], depth, parentNode, attrs);
496
392
  }
497
393
  /**
498
- * Extracts definition nodes from mdast children and populates `this.definitions`.
499
- *
500
394
  * Per CommonMark spec, the first definition for a given identifier takes
501
395
  * precedence. remark-parse emits all definition nodes in source order, so
502
396
  * we skip duplicates via `Map.has` to honour the first-wins rule.
503
- *
504
- * @param children - The root-level mdast children to scan for `definition` nodes.
505
397
  */
506
398
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
507
399
  collectDefinitions(children) {
@@ -513,15 +405,9 @@ export class MarkdownAwareParser extends Parser {
513
405
  }
514
406
  }
515
407
  /**
516
- * Computes the 1-based line number and 1-based column for a given offset.
517
- *
518
408
  * Equivalent to `getPosition()` in `@markuplint/parser-utils`, but that
519
409
  * function is not exported from the package. Kept as a standalone utility
520
410
  * to avoid coupling to parser-utils internals.
521
- *
522
- * @param source - The full source string.
523
- * @param offset - The 0-based character offset to resolve.
524
- * @returns An object with 1-based `line` and `col` values.
525
411
  */
526
412
  export function getLineAndColumn(source, offset) {
527
413
  let line = 1;
package/lib/parser.d.ts CHANGED
@@ -8,6 +8,12 @@ type MdastNode = RootContent;
8
8
  * Uses remark-parse to produce an mdast, then maps Markdown constructs
9
9
  * (headings, paragraphs, lists, links, etc.) to their corresponding HTML
10
10
  * element AST nodes. Raw HTML regions are parsed via HtmlParser.
11
+ *
12
+ * remark-parse (the unified ecosystem's de facto standard) was chosen over
13
+ * `@mdx-js/mdx`, which wraps remark-parse internally and adds about 24
14
+ * unnecessary dependencies for JS compilation that linting does not need,
15
+ * and over driving the lower-level tokenizer directly, which would require
16
+ * hand-building the mdast conversion with no practical benefit.
11
17
  */
12
18
  declare class MarkdownParser extends MarkdownAwareParser {
13
19
  #private;
package/lib/parser.js CHANGED
@@ -10,6 +10,12 @@ import { MarkdownAwareParser, getLineAndColumn } from './markdown-aware-parser.j
10
10
  * Uses remark-parse to produce an mdast, then maps Markdown constructs
11
11
  * (headings, paragraphs, lists, links, etc.) to their corresponding HTML
12
12
  * element AST nodes. Raw HTML regions are parsed via HtmlParser.
13
+ *
14
+ * remark-parse (the unified ecosystem's de facto standard) was chosen over
15
+ * `@mdx-js/mdx`, which wraps remark-parse internally and adds about 24
16
+ * unnecessary dependencies for JS compilation that linting does not need,
17
+ * and over driving the lower-level tokenizer directly, which would require
18
+ * hand-building the mdast conversion with no practical benefit.
13
19
  */
14
20
  class MarkdownParser extends MarkdownAwareParser {
15
21
  #htmlParser = new HtmlParser();
@@ -84,7 +90,20 @@ class MarkdownParser extends MarkdownAwareParser {
84
90
  offsetOffset: offset,
85
91
  offsetLine: line,
86
92
  offsetColumn: col,
93
+ // HTML embedded inside Markdown is always a partial — never a
94
+ // full document — so force fragment parsing to keep parse5 from
95
+ // emitting `missing-doctype` / `misplaced-doctype` on every
96
+ // inline HTML block. Users cannot meaningfully override this
97
+ // because there is no Markdown construct that wraps a complete
98
+ // HTML document.
99
+ documentMode: 'fragment',
87
100
  });
101
+ // Surface tokenizer-level parse errors (e.g. `duplicate-attribute`)
102
+ // collected by the embedded HtmlParser. Without this, every parse
103
+ // error inside an inline HTML block would be silently dropped on
104
+ // the way back from `#htmlParser.parse()` even though the user has
105
+ // opted in via `severity.parseError`.
106
+ this.accumulateParseErrors(doc.parseErrors);
88
107
  return [...doc.nodeList];
89
108
  }
90
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/markdown-parser",
3
- "version": "5.0.0-rc.4",
3
+ "version": "5.0.0-rc.5",
4
4
  "description": "Markdown parser for markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,7 @@
10
10
  "author": "Yusuke Hirao <yusukehirao@me.com>",
11
11
  "license": "MIT",
12
12
  "engines": {
13
- "node": ">=22"
13
+ "node": ">=24"
14
14
  },
15
15
  "type": "module",
16
16
  "exports": {
@@ -28,9 +28,9 @@
28
28
  "clean": "tsc --build --clean tsconfig.build.json"
29
29
  },
30
30
  "dependencies": {
31
- "@markuplint/html-parser": "5.0.0-rc.4",
32
- "@markuplint/ml-ast": "5.0.0-rc.4",
33
- "@markuplint/parser-utils": "5.0.0-rc.4",
31
+ "@markuplint/html-parser": "5.0.0-rc.5",
32
+ "@markuplint/ml-ast": "5.0.0-rc.5",
33
+ "@markuplint/parser-utils": "5.0.0-rc.5",
34
34
  "remark-frontmatter": "5.0.0",
35
35
  "remark-gfm": "4.0.1",
36
36
  "remark-parse": "11.0.0",
@@ -39,5 +39,5 @@
39
39
  "devDependencies": {
40
40
  "@types/mdast": "4.0.4"
41
41
  },
42
- "gitHead": "97a6339bbae23f556de5d307b3ce2ef7cfd9402d"
42
+ "gitHead": "8d87463af2ff3f1b83fb28da20f1819362cf3555"
43
43
  }
package/src/index.spec.ts CHANGED
@@ -751,6 +751,31 @@ describe('MarkdownParser', () => {
751
751
  });
752
752
  });
753
753
 
754
+ describe('Embedded HTML — parseErrors propagation (#3844)', () => {
755
+ test('emits tokenizer-level parse5 events for malformed embedded HTML', () => {
756
+ // `<div a a>` triggers parse5 `duplicate-attribute`. Even though the
757
+ // HTML lives inside Markdown, the tokenizer-level error must surface
758
+ // on `MLASTDocument.parseErrors`.
759
+ const doc = parse('# heading\n\n<div a a></div>\n');
760
+ const codes = (doc.parseErrors ?? []).map(e => e.code);
761
+ expect(codes).toContain('duplicate-attribute');
762
+ });
763
+
764
+ test('forces fragment parsing so document-level parse5 events do NOT leak from embedded HTML', () => {
765
+ // Without `documentMode: 'fragment'` forcing, a bare `<head>` in an
766
+ // inline HTML block would trip parse5's `missing-doctype` (since the
767
+ // internal HtmlParser would auto-detect document mode and complain
768
+ // about the missing `<!doctype html>`). Forcing fragment mode in
769
+ // `markdown-parser` is what prevents that leak — this test pins that
770
+ // contract.
771
+ const doc = parse('# heading\n\n<head><meta charset="utf-8"></head>\n');
772
+ const codes = (doc.parseErrors ?? []).map(e => e.code);
773
+ expect(codes).not.toContain('missing-doctype');
774
+ expect(codes).not.toContain('misplaced-doctype');
775
+ expect(codes).not.toContain('non-conforming-doctype');
776
+ });
777
+ });
778
+
754
779
  describe('getLineAndColumn', () => {
755
780
  test('returns line 1, col 1 for offset 0', () => {
756
781
  expect(getLineAndColumn('hello', 0)).toStrictEqual({ line: 1, col: 1 });