@markuplint/astro-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 +8 -0
- package/lib/astro-parser.d.ts +39 -4
- package/lib/astro-parser.js +79 -10
- package/lib/component-scanner.js +0 -9
- package/lib/detect-block-behavior.d.ts +0 -8
- package/lib/detect-block-behavior.js +0 -8
- package/lib/parser.d.ts +19 -3
- package/lib/parser.js +59 -0
- package/lib/spread-attr.d.ts +33 -0
- package/lib/spread-attr.js +109 -0
- package/package.json +5 -5
- package/ARCHITECTURE.ja.md +0 -203
- package/ARCHITECTURE.md +0 -203
- package/SKILL.md +0 -133
- package/docs/maintenance.ja.md +0 -172
- package/docs/maintenance.md +0 -172
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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
|
+
- **astro-parser:** discriminate astro-eslint-parser ParseError from Tier 1 SyntaxError ([f961bf8](https://github.com/markuplint/markuplint/commit/f961bf8a89e3713565b06ff51bd214e6d69dea3e))
|
|
11
|
+
- **astro-parser:** preserve spread attributes containing TypeScript and expression-child siblings ([0e4c64d](https://github.com/markuplint/markuplint/commit/0e4c64d90f77f9350a4895f24ba07ec90eda6546)), closes [#3856](https://github.com/markuplint/markuplint/issues/3856)
|
|
12
|
+
- **astro-parser:** stop surfacing non-fatal Astro diagnostics as parse errors ([3a2c262](https://github.com/markuplint/markuplint/commit/3a2c262316e58196838ab075d5a554751cd976ac)), closes [#3834](https://github.com/markuplint/markuplint/issues/3834) [#3823](https://github.com/markuplint/markuplint/issues/3823)
|
|
13
|
+
|
|
6
14
|
# [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
15
|
|
|
8
16
|
**Note:** Version bump only for package @markuplint/astro-parser
|
package/lib/astro-parser.d.ts
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
import type { RootNode } from '@astrojs/compiler/types';
|
|
2
|
+
/**
|
|
3
|
+
* `@astrojs/compiler` is a dev dependency used only for these type
|
|
4
|
+
* definitions; the runtime AST actually comes from `astro-eslint-parser`.
|
|
5
|
+
* When upgrading `astro-eslint-parser`, also update `@astrojs/compiler` to
|
|
6
|
+
* the version it uses internally, otherwise the types drift from the
|
|
7
|
+
* runtime AST shape.
|
|
8
|
+
*/
|
|
2
9
|
export type { RootNode, ElementNode, CustomElementNode, ComponentNode, FragmentNode, AttributeNode, Node, } from '@astrojs/compiler/types';
|
|
3
10
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
11
|
+
* ## Diagnostic handling policy
|
|
12
|
+
*
|
|
13
|
+
* Only **severity=Error** Astro diagnostics surface as a `ParserError`.
|
|
14
|
+
* Warning / Information / Hint diagnostics are passed through silently
|
|
15
|
+
* because the AST is still fully populated for those levels and Astro's own
|
|
16
|
+
* tooling (the language server, `astro check`) owns the user-facing message
|
|
17
|
+
* — markuplint must not surface them as fatal `parse-error`s (#3823).
|
|
18
|
+
*
|
|
19
|
+
* Concretely, the `is:inline` Hint that Astro emits for a `<script>` tag
|
|
20
|
+
* carrying any non-`src` attribute, and the `set:html` Warning for
|
|
21
|
+
* `set:html` overwriting children, both reach this wrapper but do not
|
|
22
|
+
* abort parsing.
|
|
23
|
+
*
|
|
24
|
+
* ## Error normalization
|
|
25
|
+
*
|
|
26
|
+
* `parseTemplate()` itself raises an `astro-eslint-parser` `ParseError`
|
|
27
|
+
* (extending `SyntaxError`) on severity=Error diagnostics and on raw
|
|
28
|
+
* template syntax errors (unterminated comments, unclosed expressions,
|
|
29
|
+
* etc.). Those — and only those — are normalized to `ParserError`, so the
|
|
30
|
+
* caller sees a single Tier-3 (per-file violation) error type for every
|
|
31
|
+
* parse failure. Any other throw, including a genuine `SyntaxError` from a
|
|
32
|
+
* markuplint invariant break, is allowed to propagate so the standard
|
|
33
|
+
* `isFatalError()` gate further up the stack can classify it as Tier 1.
|
|
34
|
+
*
|
|
35
|
+
* The defensive `find(severity === Error)` after `parseTemplate()` is dead
|
|
36
|
+
* code today (upstream gates internally) but is retained as a safety net
|
|
37
|
+
* if a future `astro-eslint-parser` ever stops gating severity=Error.
|
|
38
|
+
*
|
|
39
|
+
* @see https://github.com/markuplint/markuplint/issues/3823
|
|
40
|
+
* @see https://docs.astro.build/en/reference/directives-reference/#isinline
|
|
41
|
+
* @see https://docs.astro.build/en/guides/client-side-scripts/#script-processing
|
|
6
42
|
*
|
|
7
|
-
* @
|
|
8
|
-
* @returns The root AST node produced by the Astro compiler
|
|
43
|
+
* @throws {ParserError} on severity=Error Astro diagnostics or on raw upstream syntax errors
|
|
9
44
|
*/
|
|
10
45
|
export declare function astroParse(code: string): RootNode;
|
package/lib/astro-parser.js
CHANGED
|
@@ -1,19 +1,88 @@
|
|
|
1
1
|
import { ParserError } from '@markuplint/parser-utils';
|
|
2
2
|
import { parseTemplate } from 'astro-eslint-parser';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Astro tags each diagnostic with a VS Code-style severity
|
|
5
|
+
* (1=Error, 2=Warning, 3=Information, 4=Hint). The compiler exposes the
|
|
6
|
+
* enum as types-only via `@astrojs/compiler/types` — there is no runtime
|
|
7
|
+
* value to import — so the fatal level is mirrored as a literal here.
|
|
6
8
|
*
|
|
7
|
-
* @
|
|
8
|
-
|
|
9
|
+
* @see https://github.com/withastro/compiler/blob/main/packages/compiler/shared/types.ts
|
|
10
|
+
*/
|
|
11
|
+
const ASTRO_DIAGNOSTIC_SEVERITY_ERROR = 1;
|
|
12
|
+
/**
|
|
13
|
+
* Type guard for `astro-eslint-parser`'s `ParseError` class. The class
|
|
14
|
+
* extends `SyntaxError` and carries a unique `originalAST` instance
|
|
15
|
+
* property; the duck-type check on that property identifies it without
|
|
16
|
+
* depending on the constructor name (which a future bundler could mangle).
|
|
17
|
+
*
|
|
18
|
+
* Why a positive identity check matters: `isFatalError()` from
|
|
19
|
+
* `@markuplint/shared` classifies *every* `SyntaxError` as Tier 1
|
|
20
|
+
* (implementation bug, must propagate). The Astro upstream chose to
|
|
21
|
+
* subclass `SyntaxError` for ergonomic reasons, but the value it raises is
|
|
22
|
+
* semantically a per-file parse failure that markuplint must convert to
|
|
23
|
+
* `ParserError` (Tier 3). See `isFatalError()` in `@markuplint/shared` —
|
|
24
|
+
* Tier 1 covers only errors raised by markuplint's own code.
|
|
25
|
+
*/
|
|
26
|
+
function isAstroEslintParseError(error) {
|
|
27
|
+
return error instanceof SyntaxError && 'originalAST' in error;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* ## Diagnostic handling policy
|
|
31
|
+
*
|
|
32
|
+
* Only **severity=Error** Astro diagnostics surface as a `ParserError`.
|
|
33
|
+
* Warning / Information / Hint diagnostics are passed through silently
|
|
34
|
+
* because the AST is still fully populated for those levels and Astro's own
|
|
35
|
+
* tooling (the language server, `astro check`) owns the user-facing message
|
|
36
|
+
* — markuplint must not surface them as fatal `parse-error`s (#3823).
|
|
37
|
+
*
|
|
38
|
+
* Concretely, the `is:inline` Hint that Astro emits for a `<script>` tag
|
|
39
|
+
* carrying any non-`src` attribute, and the `set:html` Warning for
|
|
40
|
+
* `set:html` overwriting children, both reach this wrapper but do not
|
|
41
|
+
* abort parsing.
|
|
42
|
+
*
|
|
43
|
+
* ## Error normalization
|
|
44
|
+
*
|
|
45
|
+
* `parseTemplate()` itself raises an `astro-eslint-parser` `ParseError`
|
|
46
|
+
* (extending `SyntaxError`) on severity=Error diagnostics and on raw
|
|
47
|
+
* template syntax errors (unterminated comments, unclosed expressions,
|
|
48
|
+
* etc.). Those — and only those — are normalized to `ParserError`, so the
|
|
49
|
+
* caller sees a single Tier-3 (per-file violation) error type for every
|
|
50
|
+
* parse failure. Any other throw, including a genuine `SyntaxError` from a
|
|
51
|
+
* markuplint invariant break, is allowed to propagate so the standard
|
|
52
|
+
* `isFatalError()` gate further up the stack can classify it as Tier 1.
|
|
53
|
+
*
|
|
54
|
+
* The defensive `find(severity === Error)` after `parseTemplate()` is dead
|
|
55
|
+
* code today (upstream gates internally) but is retained as a safety net
|
|
56
|
+
* if a future `astro-eslint-parser` ever stops gating severity=Error.
|
|
57
|
+
*
|
|
58
|
+
* @see https://github.com/markuplint/markuplint/issues/3823
|
|
59
|
+
* @see https://docs.astro.build/en/reference/directives-reference/#isinline
|
|
60
|
+
* @see https://docs.astro.build/en/guides/client-side-scripts/#script-processing
|
|
61
|
+
*
|
|
62
|
+
* @throws {ParserError} on severity=Error Astro diagnostics or on raw upstream syntax errors
|
|
9
63
|
*/
|
|
10
64
|
export function astroParse(code) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
65
|
+
let result;
|
|
66
|
+
try {
|
|
67
|
+
({ result } = parseTemplate(code));
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (!isAstroEslintParseError(error)) {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
throw new ParserError(error.message, {
|
|
74
|
+
line: typeof error.lineNumber === 'number' ? error.lineNumber : 1,
|
|
75
|
+
col: typeof error.column === 'number' ? error.column : 0,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// Defensive: if a future `astro-eslint-parser` stops gating severity=Error
|
|
79
|
+
// diagnostics internally, surface them here so they never silently leak
|
|
80
|
+
// past the wrapper as a non-fatal pass-through.
|
|
81
|
+
const fatal = result.diagnostics.find(d => d.severity === ASTRO_DIAGNOSTIC_SEVERITY_ERROR);
|
|
82
|
+
if (fatal) {
|
|
83
|
+
throw new ParserError(fatal.text, {
|
|
84
|
+
line: fatal.location.line,
|
|
85
|
+
col: fatal.location.column,
|
|
17
86
|
});
|
|
18
87
|
}
|
|
19
88
|
return result.ast;
|
package/lib/component-scanner.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { parser } from './parser.js';
|
|
2
|
-
/**
|
|
3
|
-
* Extracts root element information from a parsed MLAST document.
|
|
4
|
-
*/
|
|
5
2
|
function extractComponentInfo(doc) {
|
|
6
3
|
const root = doc.nodeList.find((n) => n.type === 'starttag' && n.depth === 0 && !n.isFragment);
|
|
7
4
|
if (!root) {
|
|
@@ -28,15 +25,9 @@ function extractComponentInfo(doc) {
|
|
|
28
25
|
col: root.col,
|
|
29
26
|
};
|
|
30
27
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Detects whether the parsed Astro template contains `<slot>` elements.
|
|
33
|
-
*/
|
|
34
28
|
function detectSlots(doc) {
|
|
35
29
|
return doc.nodeList.some(n => n.type === 'starttag' && n.nodeName === 'slot');
|
|
36
30
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Extracts the frontmatter block (`---...---`) from an Astro component source.
|
|
39
|
-
*/
|
|
40
31
|
function extractAstroFrontmatter(source) {
|
|
41
32
|
const re = /^(?:\s*\n)?---\r?\n/;
|
|
42
33
|
const startMatch = re.exec(source);
|
|
@@ -1,10 +1,2 @@
|
|
|
1
1
|
import type { MLASTBlockBehavior } from '@markuplint/ml-ast';
|
|
2
|
-
/**
|
|
3
|
-
* Detects the block behavior of an Astro expression by inspecting its raw
|
|
4
|
-
* source for `.map()` or `.filter()` array method calls. Maps `.map()` to
|
|
5
|
-
* `'each'` (iteration) and `.filter()` to `'if'` (conditional filtering).
|
|
6
|
-
*
|
|
7
|
-
* @param raw - The raw source text of the expression to analyze
|
|
8
|
-
* @returns The detected block behavior, or `null` if no recognized pattern is found
|
|
9
|
-
*/
|
|
10
2
|
export declare function detectBlockBehavior(raw: string): MLASTBlockBehavior | null;
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detects the block behavior of an Astro expression by inspecting its raw
|
|
3
|
-
* source for `.map()` or `.filter()` array method calls. Maps `.map()` to
|
|
4
|
-
* `'each'` (iteration) and `.filter()` to `'if'` (conditional filtering).
|
|
5
|
-
*
|
|
6
|
-
* @param raw - The raw source text of the expression to analyze
|
|
7
|
-
* @returns The detected block behavior, or `null` if no recognized pattern is found
|
|
8
|
-
*/
|
|
9
1
|
export function detectBlockBehavior(raw) {
|
|
10
2
|
const re = /\.+\s*(?<type>map|filter)\s*\((?:function\s*\(.[^\n\r{\u2028\u2029]*\{.*return\s*$|.+=>\s*\(?\s*)/;
|
|
11
3
|
const match = raw.match(re);
|
package/lib/parser.d.ts
CHANGED
|
@@ -7,6 +7,11 @@ import { Parser } from '@markuplint/parser-utils';
|
|
|
7
7
|
* Extends the base Parser to handle Astro-specific syntax including frontmatter blocks,
|
|
8
8
|
* expression containers (`{}`), component/element/fragment types, Astro directives
|
|
9
9
|
* (e.g., `class:list`, `set:html`), and shorthand attributes.
|
|
10
|
+
*
|
|
11
|
+
* When forward-porting a fix from the `v4` branch, beware the `Token` field
|
|
12
|
+
* rename: v4 uses `startOffset` / `startLine` / `startCol` where this branch
|
|
13
|
+
* uses `offset` / `line` / `col` (also in AST properties asserted in spec
|
|
14
|
+
* files). The build surfaces mismatches as `TS2339`.
|
|
10
15
|
*/
|
|
11
16
|
declare class AstroParser extends Parser<Node> {
|
|
12
17
|
constructor();
|
|
@@ -31,6 +36,17 @@ declare class AstroParser extends Parser<Node> {
|
|
|
31
36
|
* the start tag, then delegating to the base visitElement with Astro-specific
|
|
32
37
|
* options including nameless fragment support.
|
|
33
38
|
*
|
|
39
|
+
* This hands the entire element source (including body and end tag) to
|
|
40
|
+
* `parseCodeFragment()`. Raw-text safety for `<script>` and `<style>`
|
|
41
|
+
* bodies is owned by parser-utils' `parseCodeFragment()` (its
|
|
42
|
+
* `rawTextElements` handling per HTML LS 13.2.5.1); without it, HTML-like
|
|
43
|
+
* substrings in a script body (e.g. a regex matching a `<br>` tag) would
|
|
44
|
+
* be re-tokenized as tags and throw `Invalid tag syntax` (#3825). If that
|
|
45
|
+
* regression reappears, the fix belongs in parser-utils, not here.
|
|
46
|
+
*
|
|
47
|
+
* @see https://github.com/markuplint/markuplint/issues/3825
|
|
48
|
+
* @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
|
|
49
|
+
*
|
|
34
50
|
* @param token - The child token representing the element
|
|
35
51
|
* @param childNodes - The child Astro AST nodes within the element
|
|
36
52
|
* @returns An array of markuplint node tree items
|
|
@@ -49,14 +65,14 @@ declare class AstroParser extends Parser<Node> {
|
|
|
49
65
|
/**
|
|
50
66
|
* Visits an attribute token, handling Astro-specific syntax including
|
|
51
67
|
* curly-brace expression values, shorthand attributes (`{name}`),
|
|
68
|
+
* spread attributes (`{...expr}`, including TypeScript and nested
|
|
69
|
+
* expressions, see #3856; root cause originally reported as #3824),
|
|
52
70
|
* and template directives (e.g., `class:list`, `set:html`).
|
|
53
71
|
*
|
|
54
72
|
* @param token - The token representing the attribute
|
|
55
73
|
* @returns The parsed attribute node with Astro-specific metadata
|
|
56
74
|
*/
|
|
57
|
-
visitAttr(token: Token):
|
|
58
|
-
__rightText?: string;
|
|
59
|
-
}) | {
|
|
75
|
+
visitAttr(token: Token): import("@markuplint/ml-ast").MLASTSpreadAttr | {
|
|
60
76
|
isDynamicValue: true | undefined;
|
|
61
77
|
isDirective: true | undefined;
|
|
62
78
|
potentialName: string | undefined;
|
package/lib/parser.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { AttrState, Parser, ParserError } from '@markuplint/parser-utils';
|
|
2
2
|
import { astroParse } from './astro-parser.js';
|
|
3
3
|
import { detectBlockBehavior } from './detect-block-behavior.js';
|
|
4
|
+
import { extractSpreadAttribute } from './spread-attr.js';
|
|
4
5
|
/**
|
|
5
6
|
* Parser implementation for Astro component templates.
|
|
6
7
|
* Extends the base Parser to handle Astro-specific syntax including frontmatter blocks,
|
|
7
8
|
* expression containers (`{}`), component/element/fragment types, Astro directives
|
|
8
9
|
* (e.g., `class:list`, `set:html`), and shorthand attributes.
|
|
10
|
+
*
|
|
11
|
+
* When forward-porting a fix from the `v4` branch, beware the `Token` field
|
|
12
|
+
* rename: v4 uses `startOffset` / `startLine` / `startCol` where this branch
|
|
13
|
+
* uses `offset` / `line` / `col` (also in AST properties asserted in spec
|
|
14
|
+
* files). The build surfaces mismatches as `TS2339`.
|
|
9
15
|
*/
|
|
10
16
|
class AstroParser extends Parser {
|
|
11
17
|
constructor() {
|
|
12
18
|
super({
|
|
19
|
+
// Astro requires explicit closing tags like XML.
|
|
13
20
|
endTagType: 'xml',
|
|
21
|
+
// Accepts both HTML void elements and XML-style self-closing (`<Component />`).
|
|
14
22
|
selfCloseType: 'html+xml',
|
|
23
|
+
// Distinguishes components (`<MyComp>`) from HTML elements (`<div>`).
|
|
15
24
|
tagNameCaseSensitive: true,
|
|
16
25
|
});
|
|
17
26
|
}
|
|
@@ -145,6 +154,17 @@ class AstroParser extends Parser {
|
|
|
145
154
|
* the start tag, then delegating to the base visitElement with Astro-specific
|
|
146
155
|
* options including nameless fragment support.
|
|
147
156
|
*
|
|
157
|
+
* This hands the entire element source (including body and end tag) to
|
|
158
|
+
* `parseCodeFragment()`. Raw-text safety for `<script>` and `<style>`
|
|
159
|
+
* bodies is owned by parser-utils' `parseCodeFragment()` (its
|
|
160
|
+
* `rawTextElements` handling per HTML LS 13.2.5.1); without it, HTML-like
|
|
161
|
+
* substrings in a script body (e.g. a regex matching a `<br>` tag) would
|
|
162
|
+
* be re-tokenized as tags and throw `Invalid tag syntax` (#3825). If that
|
|
163
|
+
* regression reappears, the fix belongs in parser-utils, not here.
|
|
164
|
+
*
|
|
165
|
+
* @see https://github.com/markuplint/markuplint/issues/3825
|
|
166
|
+
* @see https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions
|
|
167
|
+
*
|
|
148
168
|
* @param token - The child token representing the element
|
|
149
169
|
* @param childNodes - The child Astro AST nodes within the element
|
|
150
170
|
* @returns An array of markuplint node tree items
|
|
@@ -196,12 +216,45 @@ class AstroParser extends Parser {
|
|
|
196
216
|
/**
|
|
197
217
|
* Visits an attribute token, handling Astro-specific syntax including
|
|
198
218
|
* curly-brace expression values, shorthand attributes (`{name}`),
|
|
219
|
+
* spread attributes (`{...expr}`, including TypeScript and nested
|
|
220
|
+
* expressions, see #3856; root cause originally reported as #3824),
|
|
199
221
|
* and template directives (e.g., `class:list`, `set:html`).
|
|
200
222
|
*
|
|
201
223
|
* @param token - The token representing the attribute
|
|
202
224
|
* @returns The parsed attribute node with Astro-specific metadata
|
|
203
225
|
*/
|
|
204
226
|
visitAttr(token) {
|
|
227
|
+
// The spread pre-pass MUST run before `super.visitAttr()`: falling
|
|
228
|
+
// through to the base path routes the token through the espree-based
|
|
229
|
+
// `safeScriptParser`, which reproduces the truncation and
|
|
230
|
+
// `Invalid tag syntax` failures of #3824 / #3856.
|
|
231
|
+
const spreadHit = extractSpreadAttribute(token.raw);
|
|
232
|
+
if (spreadHit) {
|
|
233
|
+
let spreadLine = token.line;
|
|
234
|
+
let spreadCol = token.col;
|
|
235
|
+
for (const c of spreadHit.leadingSpace) {
|
|
236
|
+
if (c === '\n') {
|
|
237
|
+
spreadLine++;
|
|
238
|
+
spreadCol = 1;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
spreadCol++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const spread = super.visitSpreadAttr({
|
|
245
|
+
raw: spreadHit.spreadRaw,
|
|
246
|
+
offset: token.offset + spreadHit.leadingSpace.length,
|
|
247
|
+
line: spreadLine,
|
|
248
|
+
col: spreadCol,
|
|
249
|
+
});
|
|
250
|
+
// `extractSpreadAttribute` already validates the `{...EXPR}` shape
|
|
251
|
+
// so `super.visitSpreadAttr` is expected to return a node here.
|
|
252
|
+
// Falling through to the generic attr path is a defensive safeguard
|
|
253
|
+
// against future shape changes in the parent class.
|
|
254
|
+
if (spread) {
|
|
255
|
+
return spreadHit.leftover ? { ...spread, __rightText: spreadHit.leftover } : spread;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
205
258
|
const attr = super.visitAttr(token, {
|
|
206
259
|
quoteSet: [
|
|
207
260
|
{ start: '"', end: '"', type: 'string' },
|
|
@@ -224,6 +277,12 @@ class AstroParser extends Parser {
|
|
|
224
277
|
/**
|
|
225
278
|
* Detects Template Directive
|
|
226
279
|
*
|
|
280
|
+
* `class:` is special-cased with `potentialName: 'class'` so markuplint
|
|
281
|
+
* rules for the standard `class` attribute still apply to `class:list`.
|
|
282
|
+
* Every other `prefix:name` pattern gets `isDirective: true`, which
|
|
283
|
+
* tells markuplint it is framework-specific and must not be validated
|
|
284
|
+
* as a standard HTML attribute.
|
|
285
|
+
*
|
|
227
286
|
* @see https://docs.astro.build/en/reference/directives-reference/
|
|
228
287
|
*/
|
|
229
288
|
const [, directive] = attr.name.raw.match(/^([^:]+):([^:]+)$/) ?? [];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why this exists: `safeScriptParser` (espree-based) does not understand
|
|
3
|
+
* TypeScript syntax such as `{...x as any}` and may also extend a "valid JS
|
|
4
|
+
* prefix" past the spread's closing brace into surrounding HTML
|
|
5
|
+
* (e.g. `{...props}>{label}` is parsed as a binary `>` expression),
|
|
6
|
+
* misclassifying both the spread end and any expression-child siblings.
|
|
7
|
+
* See https://github.com/markuplint/markuplint/issues/3824 (v4) and
|
|
8
|
+
* https://github.com/markuplint/markuplint/issues/3856 (dev/v5).
|
|
9
|
+
*
|
|
10
|
+
* Known limitation: regular-expression literals containing braces
|
|
11
|
+
* (e.g. `{...x.match(/}/) ? a : b}`) are not recognised — `/` is always
|
|
12
|
+
* treated as a division operator. Such patterns are vanishingly rare in
|
|
13
|
+
* Astro spread attributes; rewrite via a variable indirection if needed.
|
|
14
|
+
*
|
|
15
|
+
* This is intentionally a minimal brace matcher, not a full JavaScript
|
|
16
|
+
* lexer: when a new edge case is reported, extend the string / comment /
|
|
17
|
+
* escape branches with the minimum change rather than introducing a lexer.
|
|
18
|
+
*
|
|
19
|
+
* Retraction condition: if parser-utils' `script-parser.ts` is upgraded to
|
|
20
|
+
* handle TypeScript syntax and to stop extending past the spread's closing
|
|
21
|
+
* `}`, this module and the `visitAttr()` pre-pass in `parser.ts` can be
|
|
22
|
+
* removed and the base parser path restored.
|
|
23
|
+
*/
|
|
24
|
+
export declare function findMatchingBrace(raw: string, start: number): number;
|
|
25
|
+
/**
|
|
26
|
+
* Exported so the brace-matching logic can be unit-tested independently of
|
|
27
|
+
* the parser pipeline.
|
|
28
|
+
*/
|
|
29
|
+
export declare function extractSpreadAttribute(raw: string): {
|
|
30
|
+
leadingSpace: string;
|
|
31
|
+
spreadRaw: string;
|
|
32
|
+
leftover: string;
|
|
33
|
+
} | null;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function countPrecedingBackslashes(raw, i) {
|
|
2
|
+
let n = 0;
|
|
3
|
+
let j = i - 1;
|
|
4
|
+
while (j >= 0 && raw[j] === '\\') {
|
|
5
|
+
n++;
|
|
6
|
+
j--;
|
|
7
|
+
}
|
|
8
|
+
return n;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Why this exists: `safeScriptParser` (espree-based) does not understand
|
|
12
|
+
* TypeScript syntax such as `{...x as any}` and may also extend a "valid JS
|
|
13
|
+
* prefix" past the spread's closing brace into surrounding HTML
|
|
14
|
+
* (e.g. `{...props}>{label}` is parsed as a binary `>` expression),
|
|
15
|
+
* misclassifying both the spread end and any expression-child siblings.
|
|
16
|
+
* See https://github.com/markuplint/markuplint/issues/3824 (v4) and
|
|
17
|
+
* https://github.com/markuplint/markuplint/issues/3856 (dev/v5).
|
|
18
|
+
*
|
|
19
|
+
* Known limitation: regular-expression literals containing braces
|
|
20
|
+
* (e.g. `{...x.match(/}/) ? a : b}`) are not recognised — `/` is always
|
|
21
|
+
* treated as a division operator. Such patterns are vanishingly rare in
|
|
22
|
+
* Astro spread attributes; rewrite via a variable indirection if needed.
|
|
23
|
+
*
|
|
24
|
+
* This is intentionally a minimal brace matcher, not a full JavaScript
|
|
25
|
+
* lexer: when a new edge case is reported, extend the string / comment /
|
|
26
|
+
* escape branches with the minimum change rather than introducing a lexer.
|
|
27
|
+
*
|
|
28
|
+
* Retraction condition: if parser-utils' `script-parser.ts` is upgraded to
|
|
29
|
+
* handle TypeScript syntax and to stop extending past the spread's closing
|
|
30
|
+
* `}`, this module and the `visitAttr()` pre-pass in `parser.ts` can be
|
|
31
|
+
* removed and the base parser path restored.
|
|
32
|
+
*/
|
|
33
|
+
export function findMatchingBrace(raw, start) {
|
|
34
|
+
if (raw[start] !== '{')
|
|
35
|
+
return -1;
|
|
36
|
+
let depth = 0;
|
|
37
|
+
let inString = null;
|
|
38
|
+
const templateBraceStack = [];
|
|
39
|
+
for (let i = start; i < raw.length; i++) {
|
|
40
|
+
const c = raw[i];
|
|
41
|
+
if (inString) {
|
|
42
|
+
if (inString === '`') {
|
|
43
|
+
if (c === '`' && countPrecedingBackslashes(raw, i) % 2 === 0) {
|
|
44
|
+
inString = null;
|
|
45
|
+
}
|
|
46
|
+
else if (c === '$' && raw[i + 1] === '{') {
|
|
47
|
+
templateBraceStack.push(depth);
|
|
48
|
+
inString = null;
|
|
49
|
+
depth++;
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (c === inString && countPrecedingBackslashes(raw, i) % 2 === 0) {
|
|
54
|
+
inString = null;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (c === '/' && raw[i + 1] === '/') {
|
|
59
|
+
while (i < raw.length && raw[i] !== '\n')
|
|
60
|
+
i++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (c === '/' && raw[i + 1] === '*') {
|
|
64
|
+
i += 2;
|
|
65
|
+
while (i < raw.length - 1 && !(raw[i] === '*' && raw[i + 1] === '/'))
|
|
66
|
+
i++;
|
|
67
|
+
i++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
71
|
+
inString = c;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (c === '{') {
|
|
75
|
+
depth++;
|
|
76
|
+
}
|
|
77
|
+
else if (c === '}') {
|
|
78
|
+
depth--;
|
|
79
|
+
if (depth === 0)
|
|
80
|
+
return i;
|
|
81
|
+
if (templateBraceStack.length > 0 && depth === templateBraceStack.at(-1)) {
|
|
82
|
+
templateBraceStack.pop();
|
|
83
|
+
inString = '`';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return -1;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Exported so the brace-matching logic can be unit-tested independently of
|
|
91
|
+
* the parser pipeline.
|
|
92
|
+
*/
|
|
93
|
+
export function extractSpreadAttribute(raw) {
|
|
94
|
+
const leadingMatch = /^\s*/.exec(raw);
|
|
95
|
+
const leadingSpace = leadingMatch?.[0] ?? '';
|
|
96
|
+
const remaining = raw.slice(leadingSpace.length);
|
|
97
|
+
// eslint-disable-next-line regexp/strict
|
|
98
|
+
if (!/^{\s*\.{3}[^.]/.test(remaining)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const end = findMatchingBrace(remaining, 0);
|
|
102
|
+
if (end < 0)
|
|
103
|
+
return null;
|
|
104
|
+
return {
|
|
105
|
+
leadingSpace,
|
|
106
|
+
spreadRaw: remaining.slice(0, end + 1),
|
|
107
|
+
leftover: remaining.slice(end + 1),
|
|
108
|
+
};
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/astro-parser",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.5",
|
|
4
4
|
"description": "astro 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": ">=
|
|
13
|
+
"node": ">=24"
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@markuplint/ml-ast": "5.0.0-rc.
|
|
36
|
-
"@markuplint/parser-utils": "5.0.0-rc.
|
|
35
|
+
"@markuplint/ml-ast": "5.0.0-rc.5",
|
|
36
|
+
"@markuplint/parser-utils": "5.0.0-rc.5",
|
|
37
37
|
"astro-eslint-parser": "1.4.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@astrojs/compiler": "3.0.1"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "8d87463af2ff3f1b83fb28da20f1819362cf3555"
|
|
43
43
|
}
|