@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 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
@@ -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
- * Parses an Astro component source string into the Astro compiler's root AST node.
5
- * Delegates to astro-eslint-parser and converts any diagnostics into ParserErrors.
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
- * @param code - The raw Astro component source code
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;
@@ -1,19 +1,88 @@
1
1
  import { ParserError } from '@markuplint/parser-utils';
2
2
  import { parseTemplate } from 'astro-eslint-parser';
3
3
  /**
4
- * Parses an Astro component source string into the Astro compiler's root AST node.
5
- * Delegates to astro-eslint-parser and converts any diagnostics into ParserErrors.
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
- * @param code - The raw Astro component source code
8
- * @returns The root AST node produced by the Astro compiler
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
- const { result } = parseTemplate(code);
12
- if (result.diagnostics[0]) {
13
- const error = result.diagnostics[0];
14
- throw new ParserError(error.text, {
15
- line: error.location.line,
16
- col: error.location.column,
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;
@@ -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): (import("@markuplint/ml-ast").MLASTSpreadAttr & {
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.4",
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": ">=22"
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.4",
36
- "@markuplint/parser-utils": "5.0.0-rc.4",
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": "97a6339bbae23f556de5d307b3ce2ef7cfd9402d"
42
+ "gitHead": "8d87463af2ff3f1b83fb28da20f1819362cf3555"
43
43
  }