@markuplint/astro-parser 4.6.23 → 5.0.0-alpha.1

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.
@@ -109,9 +109,9 @@ Astro の式(`{expression}`)は Astro AST で `type: 'expression'` ノード
109
109
 
110
110
  式が HTML 要素を含む場合(例: `{list.map(item => <li>{item}</li>)}`)、パーサーは複数のノードに分割します:
111
111
 
112
- 1. **開始式フラグメント**: `{list.map(item => ` — 子ノードを含む MustacheTag psblock
112
+ 1. **開始式フラグメント**: `{list.map(item => ` — 子ノードを含む MustacheTag psblock。式が `.map()` または `.filter()` 呼び出しを含む場合(`detectBlockBehavior()` で検出)、開始フラグメントにはそれぞれ `blockBehavior: { type: 'each' }` または `{ type: 'if' }` が設定される
113
113
  2. **ネストされた HTML 要素**: `<li>{item}</li>` — 通常の要素として処理
114
- 3. **終了式フラグメント**: `)}` — `isFragment: false` の別の MustacheTag psblock
114
+ 3. **終了式フラグメント**: `)}` — `isFragment: false` の別の MustacheTag psblock。開始フラグメントに `blockBehavior` がある場合、終了フラグメントには `blockBehavior: { type: 'end' }` が設定される
115
115
 
116
116
  分割ロジックは式の children 配列で `firstChild !== lastChild` かどうかを確認します。該当する場合:
117
117
 
package/ARCHITECTURE.md CHANGED
@@ -109,9 +109,9 @@ A simple expression like `{name}` has a single text child. The entire expression
109
109
 
110
110
  When an expression contains HTML elements (e.g., `{list.map(item => <li>{item}</li>)}`), the parser splits it into multiple nodes:
111
111
 
112
- 1. **Opening expression fragment**: `{list.map(item => ` — a MustacheTag psblock containing the child nodes
112
+ 1. **Opening expression fragment**: `{list.map(item => ` — a MustacheTag psblock containing the child nodes. If the expression contains a `.map()` or `.filter()` call (detected by `detectBlockBehavior()`), the opening fragment receives `blockBehavior: { type: 'each' }` or `{ type: 'if' }` respectively
113
113
  2. **Nested HTML elements**: `<li>{item}</li>` — processed as normal elements
114
- 3. **Closing expression fragment**: `)}` — a separate MustacheTag psblock with `isFragment: false`
114
+ 3. **Closing expression fragment**: `)}` — a separate MustacheTag psblock with `isFragment: false`. If the opening fragment had a `blockBehavior`, the closing fragment receives `blockBehavior: { type: 'end' }`
115
115
 
116
116
  The splitting logic checks whether `firstChild !== lastChild` in the expression's children array. If so:
117
117
 
package/CHANGELOG.md CHANGED
@@ -3,6 +3,34 @@
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-alpha.1](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.0...v5.0.0-alpha.1) (2026-02-22)
7
+
8
+ **Note:** Version bump only for package @markuplint/astro-parser
9
+
10
+ # [5.0.0-alpha.0](https://github.com/markuplint/markuplint/compare/v4.14.1...v5.0.0-alpha.0) (2026-02-20)
11
+
12
+ ### Bug Fixes
13
+
14
+ - **ml-core:** improve detection of namespace ([5b507ad](https://github.com/markuplint/markuplint/commit/5b507ad7c19c5015b8ce587845d901e31dfa6518))
15
+
16
+ - refactor(astro-parser)!: update for simplified AST token properties ([4c05de1](https://github.com/markuplint/markuplint/commit/4c05de151d30233a8d4a184c4cb70c26de19b36b))
17
+
18
+ ### Features
19
+
20
+ - **astro-parser:** support loop blocks ([ebe2eb6](https://github.com/markuplint/markuplint/commit/ebe2eb6b85aa32ff3f29964e333d058afe99d18b))
21
+
22
+ ### BREAKING CHANGES
23
+
24
+ - Adapt to renamed token properties and remove
25
+ selfClosingSolidus test.
26
+
27
+ * Token property access: startOffset -> offset, startLine -> line,
28
+ startCol -> col
29
+ * Replace selfClosingSolidus check with tagCloseChar
30
+ * Remove selfClosingSolidus test case
31
+
32
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
33
+
6
34
  ## [4.6.23](https://github.com/markuplint/markuplint/compare/@markuplint/astro-parser@4.6.22...@markuplint/astro-parser@4.6.23) (2026-02-10)
7
35
 
8
36
  **Note:** Version bump only for package @markuplint/astro-parser
@@ -1,2 +1,10 @@
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
+ */
2
10
  export declare function detectBlockBehavior(raw: string): MLASTBlockBehavior | null;
@@ -1,3 +1,11 @@
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
+ */
1
9
  export function detectBlockBehavior(raw) {
2
10
  const re = /\.+\s*(?<type>map|filter)\s*\((?:function\s*\(.[^\n\r{\u2028\u2029]*\{.*return\s*$|.+=>\s*\(?\s*)/;
3
11
  const match = raw.match(re);
package/lib/parser.d.ts CHANGED
@@ -1,19 +1,14 @@
1
1
  import type { Node } from './astro-parser.js';
2
- import type { MLASTParentNode, MLASTNodeTreeItem } from '@markuplint/ml-ast';
2
+ import type { MLASTNodeTreeItem, MLASTParentNode } from '@markuplint/ml-ast';
3
3
  import type { ChildToken, Token } from '@markuplint/parser-utils';
4
4
  import { Parser } from '@markuplint/parser-utils';
5
- type State = {
6
- scopeNS: string;
7
- };
8
5
  /**
9
6
  * Parser implementation for Astro component templates.
10
7
  * Extends the base Parser to handle Astro-specific syntax including frontmatter blocks,
11
8
  * expression containers (`{}`), component/element/fragment types, Astro directives
12
- * (e.g., `class:list`, `set:html`), shorthand attributes, and namespace-aware
13
- * element resolution (XHTML vs SVG).
9
+ * (e.g., `class:list`, `set:html`), and shorthand attributes.
14
10
  */
15
- declare class AstroParser extends Parser<Node, State> {
16
- #private;
11
+ declare class AstroParser extends Parser<Node> {
17
12
  constructor();
18
13
  tokenize(): {
19
14
  ast: Node[];
@@ -22,7 +17,7 @@ declare class AstroParser extends Parser<Node, State> {
22
17
  /**
23
18
  * Converts an Astro AST node into markuplint node tree items.
24
19
  * Handles frontmatter, doctype, text, comment, component/element/fragment,
25
- * and expression nodes. Manages namespace scoping for SVG elements.
20
+ * and expression nodes.
26
21
  *
27
22
  * @param originNode - The Astro AST node to convert
28
23
  * @param parentNode - The parent node in the markuplint tree, or null for root nodes
@@ -34,7 +29,7 @@ declare class AstroParser extends Parser<Node, State> {
34
29
  /**
35
30
  * Visits an element token by first parsing the raw HTML fragment to extract
36
31
  * the start tag, then delegating to the base visitElement with Astro-specific
37
- * options including namespace scoping and nameless fragment support.
32
+ * options including nameless fragment support.
38
33
  *
39
34
  * @param token - The child token representing the element
40
35
  * @param childNodes - The child Astro AST nodes within the element
@@ -81,12 +76,9 @@ declare class AstroParser extends Parser<Node, State> {
81
76
  isDuplicatable: boolean;
82
77
  uuid: string;
83
78
  raw: string;
84
- startOffset: number;
85
- endOffset: number;
86
- startLine: number;
87
- endLine: number;
88
- startCol: number;
89
- endCol: number;
79
+ offset: number;
80
+ line: number;
81
+ col: number;
90
82
  __rightText?: string;
91
83
  };
92
84
  /**
package/lib/parser.js CHANGED
@@ -1,17 +1,11 @@
1
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
- };
6
- var _AstroParser_instances, _AstroParser_updateScopeNS;
7
1
  import { AttrState, Parser, ParserError } from '@markuplint/parser-utils';
8
2
  import { astroParse } from './astro-parser.js';
3
+ import { detectBlockBehavior } from './detect-block-behavior.js';
9
4
  /**
10
5
  * Parser implementation for Astro component templates.
11
6
  * Extends the base Parser to handle Astro-specific syntax including frontmatter blocks,
12
7
  * expression containers (`{}`), component/element/fragment types, Astro directives
13
- * (e.g., `class:list`, `set:html`), shorthand attributes, and namespace-aware
14
- * element resolution (XHTML vs SVG).
8
+ * (e.g., `class:list`, `set:html`), and shorthand attributes.
15
9
  */
16
10
  class AstroParser extends Parser {
17
11
  constructor() {
@@ -19,10 +13,7 @@ class AstroParser extends Parser {
19
13
  endTagType: 'xml',
20
14
  selfCloseType: 'html+xml',
21
15
  tagNameCaseSensitive: true,
22
- }, {
23
- scopeNS: 'http://www.w3.org/1999/xhtml',
24
16
  });
25
- _AstroParser_instances.add(this);
26
17
  }
27
18
  tokenize() {
28
19
  return {
@@ -33,7 +24,7 @@ class AstroParser extends Parser {
33
24
  /**
34
25
  * Converts an Astro AST node into markuplint node tree items.
35
26
  * Handles frontmatter, doctype, text, comment, component/element/fragment,
36
- * and expression nodes. Manages namespace scoping for SVG elements.
27
+ * and expression nodes.
37
28
  *
38
29
  * @param originNode - The Astro AST node to convert
39
30
  * @param parentNode - The parent node in the markuplint tree, or null for root nodes
@@ -46,10 +37,9 @@ class AstroParser extends Parser {
46
37
  if (!originNode.position) {
47
38
  throw new TypeError("Node doesn't have position");
48
39
  }
49
- const startOffset = originNode.position.start.offset;
40
+ const offset = originNode.position.start.offset;
50
41
  const endOffset = originNode.position.end?.offset;
51
- const token = this.sliceFragment(startOffset, endOffset);
52
- __classPrivateFieldGet(this, _AstroParser_instances, "m", _AstroParser_updateScopeNS).call(this, originNode, parentNode);
42
+ const token = this.sliceFragment(offset, endOffset);
53
43
  switch (originNode.type) {
54
44
  case 'frontmatter': {
55
45
  return this.visitPsBlock({
@@ -102,34 +92,42 @@ class AstroParser extends Parser {
102
92
  const firstChild = originNode.children.at(0);
103
93
  const lastChild = originNode.children.at(-1);
104
94
  let startExpressionRaw = token.raw;
105
- let startExpressionStartLine = token.startLine;
106
- let startExpressionStartCol = token.startCol;
95
+ let startExpressionStartLine = token.line;
96
+ let startExpressionStartCol = token.col;
107
97
  const nodes = [];
98
+ let closeExpressionLocation = null;
108
99
  if (firstChild && lastChild && firstChild !== lastChild) {
109
- const startExpressionEndOffset = firstChild.position?.end?.offset ?? endOffset ?? startOffset;
110
- const startExpressionLocation = this.sliceFragment(startOffset, startExpressionEndOffset);
100
+ const startExpressionEndOffset = firstChild.position?.end?.offset ?? endOffset ?? offset;
101
+ const startExpressionLocation = this.sliceFragment(offset, startExpressionEndOffset);
111
102
  startExpressionRaw = startExpressionLocation.raw;
112
- startExpressionStartLine = startExpressionLocation.startLine;
113
- startExpressionStartCol = startExpressionLocation.startCol;
114
- const closeExpressionLocation = this.sliceFragment(lastChild.position?.start.offset ?? startOffset, endOffset);
115
- nodes.push(...this.visitPsBlock({
116
- ...closeExpressionLocation,
117
- depth,
118
- parentNode,
119
- nodeName: 'MustacheTag',
120
- isFragment: false,
121
- }));
103
+ startExpressionStartLine = startExpressionLocation.line;
104
+ startExpressionStartCol = startExpressionLocation.col;
105
+ closeExpressionLocation = this.sliceFragment(lastChild.position?.start.offset ?? offset, endOffset);
122
106
  }
107
+ const blockBehavior = detectBlockBehavior(startExpressionRaw);
123
108
  nodes.push(...this.visitPsBlock({
124
109
  raw: startExpressionRaw,
125
- startOffset,
126
- startLine: startExpressionStartLine,
127
- startCol: startExpressionStartCol,
110
+ offset,
111
+ line: startExpressionStartLine,
112
+ col: startExpressionStartCol,
128
113
  depth,
129
114
  parentNode,
130
115
  nodeName: 'MustacheTag',
131
116
  isFragment: true,
132
- }, originNode.children));
117
+ }, originNode.children, blockBehavior), ...(closeExpressionLocation
118
+ ? this.visitPsBlock({
119
+ ...closeExpressionLocation,
120
+ depth,
121
+ parentNode,
122
+ nodeName: 'MustacheTag',
123
+ isFragment: false,
124
+ }, [], blockBehavior
125
+ ? {
126
+ type: 'end',
127
+ expression: closeExpressionLocation.raw,
128
+ }
129
+ : undefined)
130
+ : []));
133
131
  return nodes;
134
132
  }
135
133
  default: {
@@ -145,7 +143,7 @@ class AstroParser extends Parser {
145
143
  /**
146
144
  * Visits an element token by first parsing the raw HTML fragment to extract
147
145
  * the start tag, then delegating to the base visitElement with Astro-specific
148
- * options including namespace scoping and nameless fragment support.
146
+ * options including nameless fragment support.
149
147
  *
150
148
  * @param token - The child token representing the element
151
149
  * @param childNodes - The child Astro AST nodes within the element
@@ -165,11 +163,8 @@ class AstroParser extends Parser {
165
163
  return super.visitElement(startTagNode, childNodes, {
166
164
  // https://docs.astro.build/en/basics/astro-syntax/#fragments
167
165
  namelessFragment: true,
168
- overwriteProps: {
169
- namespace: this.state.scopeNS,
170
- },
171
166
  createEndTagToken: () => {
172
- if (startTagNode.selfClosingSolidus?.raw === '/') {
167
+ if (startTagNode.tagCloseChar.startsWith('/')) {
173
168
  return null;
174
169
  }
175
170
  const endTagNode = parsedNodes.at(-1);
@@ -263,17 +258,4 @@ class AstroParser extends Parser {
263
258
  return super.detectElementType(nodeName, /^[A-Z]/);
264
259
  }
265
260
  }
266
- _AstroParser_instances = new WeakSet(), _AstroParser_updateScopeNS = function _AstroParser_updateScopeNS(
267
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
268
- originNode, parentNode) {
269
- const parentNS = this.state.scopeNS;
270
- if (parentNS === 'http://www.w3.org/1999/xhtml' &&
271
- originNode.type === 'element' &&
272
- originNode.name?.toLowerCase() === 'svg') {
273
- this.state.scopeNS = 'http://www.w3.org/2000/svg';
274
- }
275
- else if (parentNS === 'http://www.w3.org/2000/svg' && parentNode && parentNode.nodeName === 'foreignObject') {
276
- this.state.scopeNS = 'http://www.w3.org/1999/xhtml';
277
- }
278
- };
279
261
  export const parser = new AstroParser();
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@markuplint/astro-parser",
3
- "version": "4.6.23",
3
+ "version": "5.0.0-alpha.1",
4
4
  "description": "astro parser for markuplint",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
7
7
  "license": "MIT",
8
+ "engines": {
9
+ "node": ">=22"
10
+ },
8
11
  "type": "module",
9
12
  "exports": {
10
13
  ".": {
@@ -21,12 +24,12 @@
21
24
  "clean": "tsc --build --clean tsconfig.build.json"
22
25
  },
23
26
  "dependencies": {
24
- "@markuplint/ml-ast": "4.4.11",
25
- "@markuplint/parser-utils": "4.8.11",
26
- "astro-eslint-parser": "1.2.2"
27
+ "@markuplint/ml-ast": "5.0.0-alpha.1",
28
+ "@markuplint/parser-utils": "5.0.0-alpha.1",
29
+ "astro-eslint-parser": "1.3.0"
27
30
  },
28
31
  "devDependencies": {
29
32
  "@astrojs/compiler": "2.13.1"
30
33
  },
31
- "gitHead": "193ee7c1262bbed95424e38efdf1a8e56ff049f4"
34
+ "gitHead": "78a295e73a097a1ce09c777c06fa21ab68136387"
32
35
  }