@markuplint/parser-utils 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 +11 -0
- package/README.md +0 -6
- package/lib/const.d.ts +0 -9
- package/lib/const.js +0 -9
- package/lib/decision.d.ts +0 -6
- package/lib/decision.js +0 -6
- package/lib/ignore-block.d.ts +9 -0
- package/lib/ignore-block.js +14 -1
- package/lib/parser.d.ts +68 -2
- package/lib/parser.js +165 -23
- package/lib/sort-nodes.d.ts +0 -8
- package/lib/sort-nodes.js +0 -8
- package/lib/types.d.ts +8 -1
- package/package.json +8 -8
- package/ARCHITECTURE.ja.md +0 -208
- package/ARCHITECTURE.md +0 -251
- package/SKILL.md +0 -127
- package/docs/maintenance.ja.md +0 -197
- package/docs/maintenance.md +0 -197
- package/docs/parser-class.ja.md +0 -666
- package/docs/parser-class.md +0 -666
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
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
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **parser-utils:** handle raw-text element body in parseCodeFragment per HTML LS §13.2.5.1 ([4a1b0f7](https://github.com/markuplint/markuplint/commit/4a1b0f701c5c0eec91b325c4f3a9f9fb773766e8)), closes [#3825](https://github.com/markuplint/markuplint/issues/3825)
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- **parser-utils:** introduce accumulateParseErrors() for embedded parser delegation ([49e15d6](https://github.com/markuplint/markuplint/commit/49e15d6f528282ca145e77baf03ae8f6561b2b09)), closes [#3844](https://github.com/markuplint/markuplint/issues/3844)
|
|
15
|
+
- **parser-utils:** propagate Tokenized.parseErrors onto MLASTDocument ([37c8430](https://github.com/markuplint/markuplint/commit/37c8430f454d24d691e093fda9e9fb71b658b4d2)), closes [#3844](https://github.com/markuplint/markuplint/issues/3844)
|
|
16
|
+
|
|
6
17
|
# [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
18
|
|
|
8
19
|
**Note:** Version bump only for package @markuplint/parser-utils
|
package/README.md
CHANGED
|
@@ -16,9 +16,3 @@ $ yarn add @markuplint/parser-utils
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
</details>
|
|
19
|
-
|
|
20
|
-
## Documentation
|
|
21
|
-
|
|
22
|
-
- [Architecture](ARCHITECTURE.md) ([日本語](ARCHITECTURE.ja.md)) — Package overview, module relationships, and integration points
|
|
23
|
-
- [Parser Class Reference](docs/parser-class.md) ([日本語](docs/parser-class.ja.md)) — Complete reference for the abstract `Parser` class
|
|
24
|
-
- [Maintenance Guide](docs/maintenance.md) ([日本語](docs/maintenance.ja.md)) — Commands, recipes, and troubleshooting
|
package/lib/const.d.ts
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
export declare const MASK_CHAR = "\uE000";
|
|
2
2
|
/**
|
|
3
|
-
* SVG Element list
|
|
4
|
-
*
|
|
5
3
|
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element
|
|
6
4
|
*/
|
|
7
5
|
export declare const svgElementList: string[];
|
|
8
6
|
export declare const reTagName: RegExp;
|
|
9
7
|
export declare const reSplitterTag: RegExp;
|
|
10
|
-
/**
|
|
11
|
-
* - U+0009 CHARACTER TABULATION (tab) => `\t`
|
|
12
|
-
* - U+000A LINE FEED (LF) => `\n`
|
|
13
|
-
* - U+000C FORM FEED (FF) => `\f`
|
|
14
|
-
* - U+000D CARRIAGE RETURN (CR) => `\r`
|
|
15
|
-
* - U+0020 SPACE => ` `
|
|
16
|
-
*/
|
|
17
8
|
export declare const defaultSpaces: readonly ["\t", "\n", "\f", "\r", " "];
|
package/lib/const.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export const MASK_CHAR = '\uE000';
|
|
2
2
|
/**
|
|
3
|
-
* SVG Element list
|
|
4
|
-
*
|
|
5
3
|
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element
|
|
6
4
|
*/
|
|
7
5
|
export const svgElementList = [
|
|
@@ -97,11 +95,4 @@ export const svgElementList = [
|
|
|
97
95
|
// eslint-disable-next-line no-control-regex -- WHATWG HTML spec requires matching NULL character
|
|
98
96
|
export const reTagName = /^[a-z][^\0\t\n\f />]*/i;
|
|
99
97
|
export const reSplitterTag = /<[^>]+>/g;
|
|
100
|
-
/**
|
|
101
|
-
* - U+0009 CHARACTER TABULATION (tab) => `\t`
|
|
102
|
-
* - U+000A LINE FEED (LF) => `\n`
|
|
103
|
-
* - U+000C FORM FEED (FF) => `\f`
|
|
104
|
-
* - U+000D CARRIAGE RETURN (CR) => `\r`
|
|
105
|
-
* - U+0020 SPACE => ` `
|
|
106
|
-
*/
|
|
107
98
|
export const defaultSpaces = ['\t', '\n', '\f', '\r', ' '];
|
package/lib/decision.d.ts
CHANGED
package/lib/decision.js
CHANGED
package/lib/ignore-block.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { Parser } from './parser.js';
|
|
2
2
|
import type { IgnoreBlock, IgnoreTag } from './types.js';
|
|
3
3
|
import type { MLASTNodeTreeItem } from '@markuplint/ml-ast';
|
|
4
|
+
/**
|
|
5
|
+
* Masks the source regions that match the given tag patterns so that
|
|
6
|
+
* template expressions do not interfere with HTML tokenization.
|
|
7
|
+
*
|
|
8
|
+
* Invariant: the masked output must keep exactly the same character count
|
|
9
|
+
* and line breaks as the original source — `restoreNode` and all position
|
|
10
|
+
* reporting depend on offsets and line numbers in the masked code matching
|
|
11
|
+
* the original.
|
|
12
|
+
*/
|
|
4
13
|
export declare function ignoreBlock(source: string, tags: readonly IgnoreTag[], maskChar?: string): IgnoreBlock;
|
|
5
14
|
export declare function restoreNode(parser: Parser<any, any>, nodeList: readonly MLASTNodeTreeItem[], ignoreBlock: IgnoreBlock, throwErrorWhenTagHasUnresolved?: boolean): MLASTNodeTreeItem[];
|
package/lib/ignore-block.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { MASK_CHAR } from './const.js';
|
|
2
2
|
import { getPosition } from './get-location.js';
|
|
3
3
|
import { ParserError } from './parser-error.js';
|
|
4
|
+
/**
|
|
5
|
+
* Masks the source regions that match the given tag patterns so that
|
|
6
|
+
* template expressions do not interfere with HTML tokenization.
|
|
7
|
+
*
|
|
8
|
+
* Invariant: the masked output must keep exactly the same character count
|
|
9
|
+
* and line breaks as the original source — `restoreNode` and all position
|
|
10
|
+
* reporting depend on offsets and line numbers in the masked code matching
|
|
11
|
+
* the original.
|
|
12
|
+
*/
|
|
4
13
|
export function ignoreBlock(source, tags, maskChar = MASK_CHAR) {
|
|
5
14
|
let replaced = source;
|
|
6
15
|
const stack = [];
|
|
@@ -9,6 +18,11 @@ export function ignoreBlock(source, tags, maskChar = MASK_CHAR) {
|
|
|
9
18
|
const mask = maskChar.repeat(startTag.length) +
|
|
10
19
|
taggedCode.replaceAll(/[^\n]/g, maskChar) +
|
|
11
20
|
maskChar.repeat((endTag ?? '').length);
|
|
21
|
+
// Wrap in `<!` ... `>` (bogus comment syntax) so the HTML tokenizer
|
|
22
|
+
// consumes the masked region as a single bogus comment node instead of
|
|
23
|
+
// interpreting it as text or markup. The slices drop three mask
|
|
24
|
+
// characters to compensate for the three wrapper characters,
|
|
25
|
+
// preserving the total length.
|
|
12
26
|
const taggedMask = `<!${mask.slice(2).slice(0, -1)}>`;
|
|
13
27
|
return taggedMask;
|
|
14
28
|
});
|
|
@@ -165,7 +179,6 @@ ignoreBlock, throwErrorWhenTagHasUnresolved = true) {
|
|
|
165
179
|
attr.value.raw +
|
|
166
180
|
attr.endQuote.raw);
|
|
167
181
|
}
|
|
168
|
-
// Update node raw
|
|
169
182
|
const length = attr.raw.length;
|
|
170
183
|
const offset = attr.offset - node.offset;
|
|
171
184
|
const above = node.raw.slice(0, offset);
|
package/lib/parser.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Token, ChildToken, QuoteSet, ParseOptions, ParserOptions, Tokenized, ValueType } from './types.js';
|
|
2
|
-
import type { EndTagType, MLASTDocument, MLASTParentNode, MLParser, ParserAuthoredElementNameDistinguishing, MLASTElement, MLASTElementCloseTag, MLASTToken, MLASTNodeTreeItem, MLASTTag, MLASTText, MLASTAttr, MLASTChildNode, MLASTSpreadAttr, ElementType, Walker, MLASTHTMLAttr, MLASTBlockBehavior } from '@markuplint/ml-ast';
|
|
2
|
+
import type { EndTagType, MLASTDocument, MLASTParentNode, MLParser, ParserAuthoredElementNameDistinguishing, MLASTElement, MLASTElementCloseTag, MLASTToken, MLASTNodeTreeItem, MLASTTag, MLASTText, MLASTAttr, MLASTChildNode, MLASTSpreadAttr, ElementType, Walker, MLASTHTMLAttr, MLASTBlockBehavior, MLASTParseError } from '@markuplint/ml-ast';
|
|
3
3
|
import { AttrState } from './enums.js';
|
|
4
4
|
import { ParserError } from './parser-error.js';
|
|
5
5
|
/**
|
|
@@ -8,6 +8,24 @@ import { ParserError } from './parser-error.js';
|
|
|
8
8
|
* Subclasses must implement `nodeize` to convert language-specific AST nodes
|
|
9
9
|
* into the markuplint AST format.
|
|
10
10
|
*
|
|
11
|
+
* When adding framework support, choose the lightest extension that fits:
|
|
12
|
+
*
|
|
13
|
+
* - **Spec-only package** (`ExtendedSpec`, no parser) when the framework is
|
|
14
|
+
* valid HTML plus extra attributes.
|
|
15
|
+
* - **`HtmlParser` subclass** (from `@markuplint/html-parser`) configuring
|
|
16
|
+
* only `ignoreTags` when the syntax is HTML with embedded template
|
|
17
|
+
* expressions (EJS, ERB, Liquid, Mustache, Nunjucks, PHP, Smarty) — the
|
|
18
|
+
* expressions are masked before HTML parsing and restored as `psblock`
|
|
19
|
+
* nodes, so no external parsing library is needed.
|
|
20
|
+
* - **Direct `Parser` subclass** only when the document structure itself
|
|
21
|
+
* diverges from HTML (JSX, Vue SFC, Svelte, Pug, Astro).
|
|
22
|
+
*
|
|
23
|
+
* Direct subclasses should delegate tokenization to the framework's
|
|
24
|
+
* established parser library rather than hand-rolling one, and those
|
|
25
|
+
* libraries are chosen to span multiple major framework versions (e.g.
|
|
26
|
+
* `vue-eslint-parser` covers both Vue 2 and Vue 3 template syntax) so the
|
|
27
|
+
* parsers keep working across version ranges without frequent updates.
|
|
28
|
+
*
|
|
11
29
|
* @template Node - The language-specific AST node type produced by the tokenizer
|
|
12
30
|
* @template State - An optional parser state type that persists across tokenization
|
|
13
31
|
*/
|
|
@@ -68,6 +86,11 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
|
|
|
68
86
|
* the raw source code. The default implementation prepends offset spaces
|
|
69
87
|
* based on the parse options.
|
|
70
88
|
*
|
|
89
|
+
* Overrides must call `super.beforeParse()` first: the offset spaces
|
|
90
|
+
* prepended here are removed again by {@link Parser.afterParse}, and the
|
|
91
|
+
* two hooks must stay symmetric or position reporting for embedded code
|
|
92
|
+
* fragments (e.g., a `<template>` block inside a `.vue` file) breaks.
|
|
93
|
+
*
|
|
71
94
|
* @param rawCode - The raw source code about to be parsed
|
|
72
95
|
* @param options - Parse options that may specify offset positioning
|
|
73
96
|
* @returns The preprocessed source code to be used for tokenization
|
|
@@ -88,6 +111,9 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
|
|
|
88
111
|
* to perform final transformations on the node list. The default implementation
|
|
89
112
|
* removes any offset spaces that were prepended during preprocessing.
|
|
90
113
|
*
|
|
114
|
+
* Overrides must call `super.afterParse()` first: it is the counterpart of
|
|
115
|
+
* {@link Parser.beforeParse} and removes the offset spaces prepended there.
|
|
116
|
+
*
|
|
91
117
|
* @param nodeList - The fully parsed and flattened node list
|
|
92
118
|
* @param options - The parse options used for this parse invocation
|
|
93
119
|
* @returns The post-processed node list
|
|
@@ -259,6 +285,10 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
|
|
|
259
285
|
* Also detects spread attributes. If there is leftover text after the attribute,
|
|
260
286
|
* it is returned in the `__rightText` property for further processing.
|
|
261
287
|
*
|
|
288
|
+
* Subclass contract: overrides must call `super.visitAttr()` first — the base
|
|
289
|
+
* implementation owns the token decomposition; subclasses only post-process
|
|
290
|
+
* the returned node (e.g. directive detection).
|
|
291
|
+
*
|
|
262
292
|
* @param token - The token containing the raw attribute text and position
|
|
263
293
|
* @param options - Controls quoting behavior, value types, and the initial parser state
|
|
264
294
|
* @returns The parsed attribute AST node with an optional `__rightText` for remaining unparsed content
|
|
@@ -274,11 +304,27 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
|
|
|
274
304
|
/**
|
|
275
305
|
* Re-parses a text token to discover embedded HTML/XML tags within it,
|
|
276
306
|
* splitting the content into a sequence of tag and text AST nodes.
|
|
277
|
-
* Handles self-closing detection, depth tracking,
|
|
307
|
+
* Handles self-closing detection, depth tracking, void element recognition,
|
|
308
|
+
* and the raw-text element body short-circuit (HTML LS §13.2.5.1).
|
|
309
|
+
*
|
|
310
|
+
* Raw-text element handling only covers the elements listed in
|
|
311
|
+
* `rawTextElements` (default `['style', 'script']`). HTML LS *escapable* raw
|
|
312
|
+
* text elements — `<title>` and `<textarea>` — are intentionally NOT in the
|
|
313
|
+
* default. They allow character references (`&` etc.) in their body, and
|
|
314
|
+
* decoding is not implemented in this short-circuit. A subclass that adds
|
|
315
|
+
* them via the `rawTextElements` option will see character references passed
|
|
316
|
+
* through verbatim.
|
|
317
|
+
*
|
|
318
|
+
* `@markuplint/astro-parser` is the only downstream caller that hands a full
|
|
319
|
+
* element raw (start tag + body + end tag) to this method, so it is the only
|
|
320
|
+
* package that exercises the raw-text branch — when changing that branch,
|
|
321
|
+
* its tests are the most sensitive regression signal.
|
|
278
322
|
*
|
|
279
323
|
* @param token - The child token containing the code fragment to re-parse
|
|
280
324
|
* @param options - Controls whether nameless fragments (JSX `<>`) are recognized
|
|
281
325
|
* @returns An array of tag and text AST nodes discovered in the code fragment
|
|
326
|
+
* @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
|
|
327
|
+
* @see https://github.com/markuplint/markuplint/issues/3825
|
|
282
328
|
*/
|
|
283
329
|
parseCodeFragment(token: ChildToken, options?: {
|
|
284
330
|
readonly namelessFragment?: boolean;
|
|
@@ -388,4 +434,24 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
|
|
|
388
434
|
* @param replacementChildNodes - The replacement nodes to insert at the old child's position
|
|
389
435
|
*/
|
|
390
436
|
replaceChild(parentNode: MLASTParentNode, oldChildNode: MLASTChildNode, ...replacementChildNodes: readonly MLASTChildNode[]): void;
|
|
437
|
+
/**
|
|
438
|
+
* Subclass hook for parsers that delegate to an embedded parse() call —
|
|
439
|
+
* e.g., `@markuplint/markdown-parser` parsing inline HTML blocks via a
|
|
440
|
+
* private `HtmlParser` instance, or `@markuplint/pug-parser` re-running
|
|
441
|
+
* each raw HTML line through `HtmlInPugParser`. Push the embedded
|
|
442
|
+
* document's `parseErrors` here so the top-level `parse()` can merge
|
|
443
|
+
* them into the outer `MLASTDocument.parseErrors`.
|
|
444
|
+
*
|
|
445
|
+
* If the embedded document has no `parseErrors`, this is a no-op.
|
|
446
|
+
*
|
|
447
|
+
* @param parseErrors - Parse errors collected by the embedded parser. May be `undefined`.
|
|
448
|
+
*/
|
|
449
|
+
protected accumulateParseErrors(parseErrors: readonly MLASTParseError[] | undefined): void;
|
|
450
|
+
/**
|
|
451
|
+
* @internal Read-only snapshot of accumulated embedded parse errors.
|
|
452
|
+
* Used by {@link Parser.parse} to merge them with the top-level tokenize
|
|
453
|
+
* result. Subclasses should not call this directly — push via
|
|
454
|
+
* {@link Parser.accumulateParseErrors} instead.
|
|
455
|
+
*/
|
|
456
|
+
get embeddedParseErrors(): readonly MLASTParseError[];
|
|
391
457
|
}
|
package/lib/parser.js
CHANGED
|
@@ -18,6 +18,24 @@ const timer = new PerformanceTimer();
|
|
|
18
18
|
* Subclasses must implement `nodeize` to convert language-specific AST nodes
|
|
19
19
|
* into the markuplint AST format.
|
|
20
20
|
*
|
|
21
|
+
* When adding framework support, choose the lightest extension that fits:
|
|
22
|
+
*
|
|
23
|
+
* - **Spec-only package** (`ExtendedSpec`, no parser) when the framework is
|
|
24
|
+
* valid HTML plus extra attributes.
|
|
25
|
+
* - **`HtmlParser` subclass** (from `@markuplint/html-parser`) configuring
|
|
26
|
+
* only `ignoreTags` when the syntax is HTML with embedded template
|
|
27
|
+
* expressions (EJS, ERB, Liquid, Mustache, Nunjucks, PHP, Smarty) — the
|
|
28
|
+
* expressions are masked before HTML parsing and restored as `psblock`
|
|
29
|
+
* nodes, so no external parsing library is needed.
|
|
30
|
+
* - **Direct `Parser` subclass** only when the document structure itself
|
|
31
|
+
* diverges from HTML (JSX, Vue SFC, Svelte, Pug, Astro).
|
|
32
|
+
*
|
|
33
|
+
* Direct subclasses should delegate tokenization to the framework's
|
|
34
|
+
* established parser library rather than hand-rolling one, and those
|
|
35
|
+
* libraries are chosen to span multiple major framework versions (e.g.
|
|
36
|
+
* `vue-eslint-parser` covers both Vue 2 and Vue 3 template syntax) so the
|
|
37
|
+
* parsers keep working across version ranges without frequent updates.
|
|
38
|
+
*
|
|
21
39
|
* @template Node - The language-specific AST node type produced by the tokenizer
|
|
22
40
|
* @template State - An optional parser state type that persists across tokenization
|
|
23
41
|
*/
|
|
@@ -31,6 +49,25 @@ export class Parser {
|
|
|
31
49
|
#selfCloseType = 'html';
|
|
32
50
|
#spaceChars = defaultSpaces;
|
|
33
51
|
#rawTextElements = ['style', 'script'];
|
|
52
|
+
/**
|
|
53
|
+
* Buffer for parse errors collected from **embedded** parse() calls
|
|
54
|
+
* (e.g., Markdown's inline HTML blocks, Pug's raw HTML lines — these
|
|
55
|
+
* invoke a separate HtmlParser instance from inside `nodeize()`).
|
|
56
|
+
*
|
|
57
|
+
* Subclasses that delegate to an internal parser should push the
|
|
58
|
+
* resulting `parseErrors` onto this array via {@link Parser.accumulateParseErrors}.
|
|
59
|
+
* The base `parse()` merges them with the top-level tokenize result so
|
|
60
|
+
* the final `MLASTDocument.parseErrors` is complete.
|
|
61
|
+
*
|
|
62
|
+
* Reset on every `parse()` invocation.
|
|
63
|
+
*/
|
|
64
|
+
#embeddedParseErrors = [];
|
|
65
|
+
/**
|
|
66
|
+
* Keyed by the original tag name (as authored in source) so a
|
|
67
|
+
* `tagNameCaseSensitive` parser that preserves casing reuses the same
|
|
68
|
+
* `RegExp` for every occurrence.
|
|
69
|
+
*/
|
|
70
|
+
#rawTextCloseTagPatternCache = new Map();
|
|
34
71
|
#authoredElementName;
|
|
35
72
|
#originalRawCode = '';
|
|
36
73
|
#rawCode = '';
|
|
@@ -117,6 +154,11 @@ export class Parser {
|
|
|
117
154
|
* the raw source code. The default implementation prepends offset spaces
|
|
118
155
|
* based on the parse options.
|
|
119
156
|
*
|
|
157
|
+
* Overrides must call `super.beforeParse()` first: the offset spaces
|
|
158
|
+
* prepended here are removed again by {@link Parser.afterParse}, and the
|
|
159
|
+
* two hooks must stay symmetric or position reporting for embedded code
|
|
160
|
+
* fragments (e.g., a `<template>` block inside a `.vue` file) breaks.
|
|
161
|
+
*
|
|
120
162
|
* @param rawCode - The raw source code about to be parsed
|
|
121
163
|
* @param options - Parse options that may specify offset positioning
|
|
122
164
|
* @returns The preprocessed source code to be used for tokenization
|
|
@@ -157,6 +199,7 @@ export class Parser {
|
|
|
157
199
|
const tokenized = this.tokenize(options);
|
|
158
200
|
const ast = tokenized.ast;
|
|
159
201
|
const isFragment = tokenized.isFragment;
|
|
202
|
+
const parseErrors = tokenized.parseErrors;
|
|
160
203
|
this.#defaultDepth = options?.depth ?? this.#defaultDepth;
|
|
161
204
|
timer.push('traverse');
|
|
162
205
|
const traversed = this.traverse(ast, null, this.#defaultDepth);
|
|
@@ -205,11 +248,17 @@ export class Parser {
|
|
|
205
248
|
}
|
|
206
249
|
timer.log();
|
|
207
250
|
domLog(nodeList);
|
|
251
|
+
// Merge top-level tokenizer parseErrors with any parseErrors
|
|
252
|
+
// pushed by embedded parser delegations (Markdown / Pug HTML
|
|
253
|
+
// regions). Snapshot before #reset() clears the buffer.
|
|
254
|
+
const embeddedErrors = this.embeddedParseErrors;
|
|
255
|
+
const mergedParseErrors = parseErrors || embeddedErrors.length > 0 ? [...(parseErrors ?? []), ...embeddedErrors] : undefined;
|
|
208
256
|
this.#reset();
|
|
209
257
|
return {
|
|
210
258
|
raw: rawCode,
|
|
211
259
|
nodeList,
|
|
212
260
|
isFragment,
|
|
261
|
+
...(mergedParseErrors && mergedParseErrors.length > 0 ? { parseErrors: mergedParseErrors } : {}),
|
|
213
262
|
};
|
|
214
263
|
}
|
|
215
264
|
catch (error) {
|
|
@@ -221,6 +270,9 @@ export class Parser {
|
|
|
221
270
|
* to perform final transformations on the node list. The default implementation
|
|
222
271
|
* removes any offset spaces that were prepended during preprocessing.
|
|
223
272
|
*
|
|
273
|
+
* Overrides must call `super.afterParse()` first: it is the counterpart of
|
|
274
|
+
* {@link Parser.beforeParse} and removes the offset spaces prepended there.
|
|
275
|
+
*
|
|
224
276
|
* @param nodeList - The fully parsed and flattened node list
|
|
225
277
|
* @param options - The parse options used for this parse invocation
|
|
226
278
|
* @returns The post-processed node list
|
|
@@ -574,6 +626,10 @@ export class Parser {
|
|
|
574
626
|
* Also detects spread attributes. If there is leftover text after the attribute,
|
|
575
627
|
* it is returned in the `__rightText` property for further processing.
|
|
576
628
|
*
|
|
629
|
+
* Subclass contract: overrides must call `super.visitAttr()` first — the base
|
|
630
|
+
* implementation owns the token decomposition; subclasses only post-process
|
|
631
|
+
* the returned node (e.g. directive detection).
|
|
632
|
+
*
|
|
577
633
|
* @param token - The token containing the raw attribute text and position
|
|
578
634
|
* @param options - Controls quoting behavior, value types, and the initial parser state
|
|
579
635
|
* @returns The parsed attribute AST node with an optional `__rightText` for remaining unparsed content
|
|
@@ -646,11 +702,27 @@ export class Parser {
|
|
|
646
702
|
/**
|
|
647
703
|
* Re-parses a text token to discover embedded HTML/XML tags within it,
|
|
648
704
|
* splitting the content into a sequence of tag and text AST nodes.
|
|
649
|
-
* Handles self-closing detection, depth tracking,
|
|
705
|
+
* Handles self-closing detection, depth tracking, void element recognition,
|
|
706
|
+
* and the raw-text element body short-circuit (HTML LS §13.2.5.1).
|
|
707
|
+
*
|
|
708
|
+
* Raw-text element handling only covers the elements listed in
|
|
709
|
+
* `rawTextElements` (default `['style', 'script']`). HTML LS *escapable* raw
|
|
710
|
+
* text elements — `<title>` and `<textarea>` — are intentionally NOT in the
|
|
711
|
+
* default. They allow character references (`&` etc.) in their body, and
|
|
712
|
+
* decoding is not implemented in this short-circuit. A subclass that adds
|
|
713
|
+
* them via the `rawTextElements` option will see character references passed
|
|
714
|
+
* through verbatim.
|
|
715
|
+
*
|
|
716
|
+
* `@markuplint/astro-parser` is the only downstream caller that hands a full
|
|
717
|
+
* element raw (start tag + body + end tag) to this method, so it is the only
|
|
718
|
+
* package that exercises the raw-text branch — when changing that branch,
|
|
719
|
+
* its tests are the most sensitive regression signal.
|
|
650
720
|
*
|
|
651
721
|
* @param token - The child token containing the code fragment to re-parse
|
|
652
722
|
* @param options - Controls whether nameless fragments (JSX `<>`) are recognized
|
|
653
723
|
* @returns An array of tag and text AST nodes discovered in the code fragment
|
|
724
|
+
* @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
|
|
725
|
+
* @see https://github.com/markuplint/markuplint/issues/3825
|
|
654
726
|
*/
|
|
655
727
|
parseCodeFragment(token, options) {
|
|
656
728
|
const nodes = [];
|
|
@@ -732,6 +804,51 @@ export class Parser {
|
|
|
732
804
|
}
|
|
733
805
|
nodes.push(tag);
|
|
734
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* Raw-text element body short-circuit (HTML Living Standard §13.2.5.1
|
|
809
|
+
* — "Restrictions on the contents of raw text and escapable raw text
|
|
810
|
+
* elements". The spec section anchor is `cdata-rcdata-restrictions`,
|
|
811
|
+
* where RCDATA stands for "Raw text + Character REF data" — the broader
|
|
812
|
+
* class that covers escapable raw text such as `<title>` / `<textarea>`).
|
|
813
|
+
*
|
|
814
|
+
* Per spec, the contents of `<script>` / `<style>` (and any caller-supplied
|
|
815
|
+
* raw-text element) are NOT re-tokenized as HTML — the only thing that
|
|
816
|
+
* terminates the body is `</tagName` followed by a tab/LF/FF/CR/space/`>`
|
|
817
|
+
* /`/`. Without this guard, fragments like `<script>const t = s.replace(
|
|
818
|
+
* /<br\s*\/?>/gi, " ");</script>` would feed the regex to `#parseTag`,
|
|
819
|
+
* which would try to parse `<br\s*\/?>` as a tag and throw on the
|
|
820
|
+
* backslash (#3825).
|
|
821
|
+
*
|
|
822
|
+
* This is dormant for `jsx-parser` and `mdx-parser` because their upstream
|
|
823
|
+
* tokenizers reject bare `<` in element body before reaching here, but the
|
|
824
|
+
* fix keeps `parseCodeFragment` honest for any future caller.
|
|
825
|
+
*
|
|
826
|
+
* @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
|
|
827
|
+
* @see https://github.com/markuplint/markuplint/issues/3825
|
|
828
|
+
*/
|
|
829
|
+
if (tag.type === 'starttag' && !isSelfClose && this.#rawTextElements.includes(tag.nodeName.toLowerCase())) {
|
|
830
|
+
const closeTagPattern = this.#getRawTextCloseTagPattern(tag.nodeName);
|
|
831
|
+
const match = closeTagPattern.exec(raw);
|
|
832
|
+
if (match) {
|
|
833
|
+
const bodyRaw = raw.slice(0, match.index);
|
|
834
|
+
if (bodyRaw) {
|
|
835
|
+
const bodyToken = this.createToken(bodyRaw, curOffset, curLine, curCol);
|
|
836
|
+
const bodyNode = {
|
|
837
|
+
...bodyToken,
|
|
838
|
+
type: 'text',
|
|
839
|
+
depth,
|
|
840
|
+
nodeName: '#text',
|
|
841
|
+
parentNode: null,
|
|
842
|
+
parentNodeUuid: null,
|
|
843
|
+
};
|
|
844
|
+
nodes.push(bodyNode);
|
|
845
|
+
({ offset: curOffset, line: curLine, col: curCol } = this.#getEndLocation(bodyToken));
|
|
846
|
+
}
|
|
847
|
+
raw = raw.slice(match.index);
|
|
848
|
+
}
|
|
849
|
+
// If no matching close tag is found, fall through to the generic loop
|
|
850
|
+
// so the existing "unclosed tag" handling remains in effect.
|
|
851
|
+
}
|
|
735
852
|
}
|
|
736
853
|
return nodes;
|
|
737
854
|
}
|
|
@@ -1026,13 +1143,24 @@ export class Parser {
|
|
|
1026
1143
|
};
|
|
1027
1144
|
}
|
|
1028
1145
|
/**
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
* @param node - The node to test.
|
|
1033
|
-
* @param potentialAncestor - The node that may be an ancestor.
|
|
1034
|
-
* @returns `true` if `node` is a descendant of `potentialAncestor`.
|
|
1146
|
+
* The pattern matches `</tagName` followed by a tab/LF/FF/CR/space/`>`/`/`
|
|
1147
|
+
* (HTML LS §13.2.5.1) ASCII-case-insensitively. Caching avoids recompiling the
|
|
1148
|
+
* `RegExp` for every `<script>` / `<style>` start tag in large documents.
|
|
1035
1149
|
*/
|
|
1150
|
+
#getRawTextCloseTagPattern(tagName) {
|
|
1151
|
+
const cached = this.#rawTextCloseTagPatternCache.get(tagName);
|
|
1152
|
+
if (cached) {
|
|
1153
|
+
return cached;
|
|
1154
|
+
}
|
|
1155
|
+
const escapedName = tagName.replaceAll(/[$()*+.?[\\\]^{|}]/g, '\\$&');
|
|
1156
|
+
// `regexp/strict` cannot statically verify a pattern built from the
|
|
1157
|
+
// dynamic `escapedName` interpolation; the escape above is the contract
|
|
1158
|
+
// that makes this safe for any caller-supplied `rawTextElements` value.
|
|
1159
|
+
// eslint-disable-next-line regexp/strict
|
|
1160
|
+
const pattern = new RegExp(`</${escapedName}(?=[\\t\\n\\f\\r >/])`, 'i');
|
|
1161
|
+
this.#rawTextCloseTagPatternCache.set(tagName, pattern);
|
|
1162
|
+
return pattern;
|
|
1163
|
+
}
|
|
1036
1164
|
#isDescendantOf(node, potentialAncestor) {
|
|
1037
1165
|
let current = 'parentNode' in node ? (node.parentNode ?? null) : null;
|
|
1038
1166
|
while (current) {
|
|
@@ -1282,13 +1410,7 @@ export class Parser {
|
|
|
1282
1410
|
* @param nodeOrders [Disruptive change]
|
|
1283
1411
|
*/
|
|
1284
1412
|
#removeDeprecatedNode(nodeOrders) {
|
|
1285
|
-
/**
|
|
1286
|
-
* sorting
|
|
1287
|
-
*/
|
|
1288
1413
|
const sorted = nodeOrders.toSorted(sortNodes);
|
|
1289
|
-
/**
|
|
1290
|
-
* remove duplicated node
|
|
1291
|
-
*/
|
|
1292
1414
|
const stack = {};
|
|
1293
1415
|
const removeIndexes = [];
|
|
1294
1416
|
for (const [i, node] of sorted.entries()) {
|
|
@@ -1330,34 +1452,54 @@ export class Parser {
|
|
|
1330
1452
|
return nodeList;
|
|
1331
1453
|
}
|
|
1332
1454
|
#reset() {
|
|
1333
|
-
// Reset state
|
|
1334
1455
|
this.state = structuredClone(this.#defaultState);
|
|
1335
1456
|
this.#defaultDepth = 0;
|
|
1457
|
+
this.#embeddedParseErrors = [];
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Subclass hook for parsers that delegate to an embedded parse() call —
|
|
1461
|
+
* e.g., `@markuplint/markdown-parser` parsing inline HTML blocks via a
|
|
1462
|
+
* private `HtmlParser` instance, or `@markuplint/pug-parser` re-running
|
|
1463
|
+
* each raw HTML line through `HtmlInPugParser`. Push the embedded
|
|
1464
|
+
* document's `parseErrors` here so the top-level `parse()` can merge
|
|
1465
|
+
* them into the outer `MLASTDocument.parseErrors`.
|
|
1466
|
+
*
|
|
1467
|
+
* If the embedded document has no `parseErrors`, this is a no-op.
|
|
1468
|
+
*
|
|
1469
|
+
* @param parseErrors - Parse errors collected by the embedded parser. May be `undefined`.
|
|
1470
|
+
*/
|
|
1471
|
+
accumulateParseErrors(parseErrors) {
|
|
1472
|
+
if (parseErrors && parseErrors.length > 0) {
|
|
1473
|
+
this.#embeddedParseErrors.push(...parseErrors);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* @internal Read-only snapshot of accumulated embedded parse errors.
|
|
1478
|
+
* Used by {@link Parser.parse} to merge them with the top-level tokenize
|
|
1479
|
+
* result. Subclasses should not call this directly — push via
|
|
1480
|
+
* {@link Parser.accumulateParseErrors} instead.
|
|
1481
|
+
*/
|
|
1482
|
+
get embeddedParseErrors() {
|
|
1483
|
+
return this.#embeddedParseErrors;
|
|
1336
1484
|
}
|
|
1337
1485
|
#setRawCode(rawCode, originalRawCode) {
|
|
1338
1486
|
this.#rawCode = rawCode;
|
|
1339
1487
|
this.#originalRawCode = originalRawCode ?? this.#originalRawCode;
|
|
1340
1488
|
}
|
|
1341
1489
|
/**
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1344
|
-
* elements that occupy a later (or overlapping) source range.
|
|
1490
|
+
* Prevents text content from bleeding into adjacent elements that
|
|
1491
|
+
* occupy a later (or overlapping) source range.
|
|
1345
1492
|
*
|
|
1346
1493
|
* Skips trimming when the text node is a descendant of the next node
|
|
1347
1494
|
* in the tree hierarchy, because synthetic parsers (e.g., Markdown)
|
|
1348
1495
|
* can produce child elements that share the same source range as their
|
|
1349
1496
|
* parent and therefore appear before the parent in offset-sorted order.
|
|
1350
|
-
*
|
|
1351
|
-
* @param nodeList - The flat, offset-sorted node list to process.
|
|
1352
|
-
* @returns A new node list with overlapping text nodes trimmed.
|
|
1353
1497
|
*/
|
|
1354
1498
|
#trimText(nodeList) {
|
|
1355
1499
|
const newNodeList = [];
|
|
1356
1500
|
let prevNode = null;
|
|
1357
1501
|
for (const node of nodeList) {
|
|
1358
|
-
if (prevNode?.type === 'text' &&
|
|
1359
|
-
// Empty node
|
|
1360
|
-
node.raw.length > 0) {
|
|
1502
|
+
if (prevNode?.type === 'text' && node.raw.length > 0) {
|
|
1361
1503
|
const prevNodeEndOffset = prevNode.offset + prevNode.raw.length;
|
|
1362
1504
|
const nodeStartOffset = node.offset;
|
|
1363
1505
|
if (prevNodeEndOffset > nodeStartOffset && !this.#isDescendantOf(prevNode, node)) {
|
package/lib/sort-nodes.d.ts
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
1
|
import type { MLASTNodeTreeItem } from '@markuplint/ml-ast';
|
|
2
|
-
/**
|
|
3
|
-
* Comparator function for sorting AST nodes by their source position.
|
|
4
|
-
* Sorts primarily by offset, then by end offset for nodes at the same position.
|
|
5
|
-
*
|
|
6
|
-
* @param a - The first node to compare
|
|
7
|
-
* @param b - The second node to compare
|
|
8
|
-
* @returns A negative, zero, or positive number for sort ordering
|
|
9
|
-
*/
|
|
10
2
|
export declare function sortNodes(a: MLASTNodeTreeItem, b: MLASTNodeTreeItem): number;
|
package/lib/sort-nodes.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Comparator function for sorting AST nodes by their source position.
|
|
3
|
-
* Sorts primarily by offset, then by end offset for nodes at the same position.
|
|
4
|
-
*
|
|
5
|
-
* @param a - The first node to compare
|
|
6
|
-
* @param b - The second node to compare
|
|
7
|
-
* @returns A negative, zero, or positive number for sort ordering
|
|
8
|
-
*/
|
|
9
1
|
export function sortNodes(a, b) {
|
|
10
2
|
if (a.offset === b.offset) {
|
|
11
3
|
return sort(a.offset + a.raw.length, b.offset + b.raw.length);
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EndTagType, MLASTParentNode, ParserOptions as ConfigParserOptions } from '@markuplint/ml-ast';
|
|
1
|
+
import type { EndTagType, MLASTParentNode, MLASTParseError, ParserOptions as ConfigParserOptions } from '@markuplint/ml-ast';
|
|
2
2
|
/**
|
|
3
3
|
* Configuration options for initializing a Parser instance,
|
|
4
4
|
* controlling how the parser handles tags, attributes, and whitespace.
|
|
@@ -34,6 +34,13 @@ export type Tokenized<N extends {} = {}, State extends unknown = null> = {
|
|
|
34
34
|
readonly ast: N[];
|
|
35
35
|
readonly isFragment: boolean;
|
|
36
36
|
readonly state?: State;
|
|
37
|
+
/**
|
|
38
|
+
* Non-fatal parser conformance errors collected during tokenisation.
|
|
39
|
+
* Optional — parsers that have no equivalent surface (e.g., raw regex
|
|
40
|
+
* tokenizers) simply omit it. Propagated unchanged onto
|
|
41
|
+
* `MLASTDocument.parseErrors` by {@link Parser.parse}.
|
|
42
|
+
*/
|
|
43
|
+
readonly parseErrors?: readonly MLASTParseError[];
|
|
37
44
|
};
|
|
38
45
|
/**
|
|
39
46
|
* A minimal source token representing a raw string fragment
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/parser-utils",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.5",
|
|
4
4
|
"description": "Utility module for markuplint parser plugin",
|
|
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": ">=
|
|
13
|
+
"node": ">=24"
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
@@ -35,16 +35,16 @@
|
|
|
35
35
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@markuplint/ml-ast": "5.0.0-rc.
|
|
39
|
-
"@markuplint/ml-spec": "5.0.0-rc.
|
|
40
|
-
"@markuplint/shared": "5.0.0-rc.
|
|
41
|
-
"@markuplint/types": "5.0.0-rc.
|
|
38
|
+
"@markuplint/ml-ast": "5.0.0-rc.5",
|
|
39
|
+
"@markuplint/ml-spec": "5.0.0-rc.5",
|
|
40
|
+
"@markuplint/shared": "5.0.0-rc.5",
|
|
41
|
+
"@markuplint/types": "5.0.0-rc.5",
|
|
42
42
|
"debug": "4.4.3",
|
|
43
43
|
"espree": "11.2.0",
|
|
44
44
|
"type-fest": "5.6.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@typescript-eslint/typescript-estree": "8.
|
|
47
|
+
"@typescript-eslint/typescript-estree": "8.59.0"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "8d87463af2ff3f1b83fb28da20f1819362cf3555"
|
|
50
50
|
}
|