@markuplint/astro-parser 4.6.22 → 5.0.0-alpha.0
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/ARCHITECTURE.ja.md +229 -0
- package/ARCHITECTURE.md +229 -0
- package/CHANGELOG.md +26 -2
- package/SKILL.md +109 -0
- package/docs/maintenance.ja.md +183 -0
- package/docs/maintenance.md +183 -0
- package/lib/astro-parser.d.ts +7 -0
- package/lib/astro-parser.js +7 -0
- package/lib/detect-block-behavior.d.ts +10 -0
- package/lib/detect-block-behavior.js +20 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +7 -0
- package/lib/parser.d.ts +47 -12
- package/lib/parser.js +72 -47
- package/package.json +9 -6
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# @markuplint/astro-parser
|
|
2
|
+
|
|
3
|
+
## 概要
|
|
4
|
+
|
|
5
|
+
`@markuplint/astro-parser` は markuplint における Astro コンポーネントファイル(`.astro`)のパーサーです。`astro-eslint-parser`(`@astrojs/compiler` をラップ)を使用して Astro ソースコードをトークン化し、その結果の AST を markuplint の統一 AST 形式(`MLASTDocument`)に変換します。フロントマターブロック(`---...---`)、式コンテナ(`{expression}`)、テンプレートディレクティブ(例: `class:list`、`set:html`)、ショートハンド属性(`{prop}`)、名前空間対応の要素解決(XHTML vs SVG)など、Astro 固有の構文を処理します。
|
|
6
|
+
|
|
7
|
+
## ディレクトリ構成
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
├── index.ts — parser インスタンスを再エクスポート
|
|
12
|
+
├── parser.ts — Parser<Node, State> を拡張する AstroParser クラス
|
|
13
|
+
├── astro-parser.ts — astro-eslint-parser ラッパーと型の再エクスポート
|
|
14
|
+
├── parser.spec.ts — AstroParser 統合テスト
|
|
15
|
+
└── astro-parser.spec.ts — astro-eslint-parser ラッパーテスト
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## アーキテクチャ図
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
flowchart TD
|
|
22
|
+
subgraph upstream ["上流"]
|
|
23
|
+
mlAst["@markuplint/ml-ast\n(AST 型定義)"]
|
|
24
|
+
parserUtils["@markuplint/parser-utils\n(抽象 Parser クラス)"]
|
|
25
|
+
astroEslintParser["astro-eslint-parser\n(Astro トークナイザ)"]
|
|
26
|
+
astroCompiler["@astrojs/compiler\n(AST 型定義)"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
subgraph pkg ["@markuplint/astro-parser"]
|
|
30
|
+
astroParser["AstroParser\nextends Parser‹Node, State›"]
|
|
31
|
+
astroParseFn["astroParse()\nastro-eslint-parser ラッパー"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
subgraph downstream ["下流"]
|
|
35
|
+
mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
mlAst -->|"AST 型"| astroParser
|
|
39
|
+
parserUtils -->|"Parser 基底クラス"| astroParser
|
|
40
|
+
astroEslintParser -->|"parseTemplate()"| astroParseFn
|
|
41
|
+
astroCompiler -->|"Node 型"| astroParseFn
|
|
42
|
+
astroParseFn -->|"RootNode.children"| astroParser
|
|
43
|
+
astroParser -->|"MLASTDocument を生成"| mlCore
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## AstroParser クラス
|
|
47
|
+
|
|
48
|
+
### 継承関係
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Parser<Node, State> (@markuplint/parser-utils)
|
|
52
|
+
└── AstroParser (このパッケージ)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### コンストラクタ
|
|
56
|
+
|
|
57
|
+
コンストラクタは Astro 固有のオプションで基底 `Parser` を設定します:
|
|
58
|
+
|
|
59
|
+
| オプション | 値 | 用途 |
|
|
60
|
+
| ---------------------- | ------------ | ---------------------------------------------------------------------------- |
|
|
61
|
+
| `endTagType` | `'xml'` | Astro は XML のように明示的な閉じタグを使用 |
|
|
62
|
+
| `selfCloseType` | `'html+xml'` | HTML void 要素と XML スタイルの自己閉じ(`<Component />`)の両方を受け入れる |
|
|
63
|
+
| `tagNameCaseSensitive` | `true` | コンポーネント(`<MyComp>`)と HTML 要素(`<div>`)を区別 |
|
|
64
|
+
|
|
65
|
+
### State 型
|
|
66
|
+
|
|
67
|
+
パーサーは `State` 型を通じて内部状態を管理します:
|
|
68
|
+
|
|
69
|
+
| フィールド | 型 | 用途 |
|
|
70
|
+
| ---------- | -------- | --------------------------------------------------------------- |
|
|
71
|
+
| `scopeNS` | `string` | 現在の名前空間 URI、デフォルトは `http://www.w3.org/1999/xhtml` |
|
|
72
|
+
|
|
73
|
+
`scopeNS` 状態は `#updateScopeNS()` によってパーサーが要素を走査する際に更新され、`<svg>` 要素内で SVG 名前空間に切り替わり、`<foreignObject>` 内で XHTML に戻ります。
|
|
74
|
+
|
|
75
|
+
### オーバーライドメソッド
|
|
76
|
+
|
|
77
|
+
| メソッド | 用途 |
|
|
78
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
79
|
+
| `tokenize()` | `astroParse()` を呼び出して Astro AST を取得し、`{ ast: rootNode.children, isFragment: true }` を返す |
|
|
80
|
+
| `nodeize()` | Astro AST ノードを markuplint ノードに変換。ノードタイプ(frontmatter, doctype, text, comment, element, expression)で振り分け |
|
|
81
|
+
| `afterFlattenNodes()` | `{ exposeInvalidNode: false }` で親に委譲 |
|
|
82
|
+
| `visitElement()` | `parseCodeFragment()` で `namelessFragment: true` として生の HTML フラグメントをパースし、名前空間と終了タグ処理で親に委譲 |
|
|
83
|
+
| `visitChildren()` | 親に委譲した後、予期しない兄弟ノードが残っていないことをアサート |
|
|
84
|
+
| `visitAttr()` | 波括弧式の値、ショートハンド属性、テンプレートディレクティブを処理 |
|
|
85
|
+
| `detectElementType()` | `/^[A-Z]/` パターンでコンポーネントと HTML 要素を検出(大文字始まりの名前はコンポーネント) |
|
|
86
|
+
|
|
87
|
+
## フロントマター処理
|
|
88
|
+
|
|
89
|
+
Astro コンポーネントは `---` で区切られたフロントマターブロックを含むことができます:
|
|
90
|
+
|
|
91
|
+
```astro
|
|
92
|
+
---
|
|
93
|
+
const name = "World";
|
|
94
|
+
---
|
|
95
|
+
<div>{name}</div>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`astro-eslint-parser` は `type: 'frontmatter'` のノードを生成します。パーサーはこれを `nodeName: 'Frontmatter'` かつ `isFragment: false` の **psblock**(疑似ブロック)に変換します。区切り文字 `---` を含むブロック全体が単一の不透明ノードとしてキャプチャされます。フロントマター内のコンテンツは HTML としてパースされません。
|
|
99
|
+
|
|
100
|
+
## 式の処理
|
|
101
|
+
|
|
102
|
+
Astro の式(`{expression}`)は Astro AST で `type: 'expression'` ノードとして表現されます。パーサーはこれらを **MustacheTag** psblock ノードに変換します。
|
|
103
|
+
|
|
104
|
+
### 単純な式
|
|
105
|
+
|
|
106
|
+
`{name}` のような単純な式は単一のテキスト子ノードを持ちます。式全体が `isFragment: true` の1つの MustacheTag psblock として出力されます。
|
|
107
|
+
|
|
108
|
+
### HTML を含むネストされた式
|
|
109
|
+
|
|
110
|
+
式が HTML 要素を含む場合(例: `{list.map(item => <li>{item}</li>)}`)、パーサーは複数のノードに分割します:
|
|
111
|
+
|
|
112
|
+
1. **開始式フラグメント**: `{list.map(item => ` — 子ノードを含む MustacheTag psblock。式が `.map()` または `.filter()` 呼び出しを含む場合(`detectBlockBehavior()` で検出)、開始フラグメントにはそれぞれ `blockBehavior: { type: 'each' }` または `{ type: 'if' }` が設定される
|
|
113
|
+
2. **ネストされた HTML 要素**: `<li>{item}</li>` — 通常の要素として処理
|
|
114
|
+
3. **終了式フラグメント**: `)}` — `isFragment: false` の別の MustacheTag psblock。開始フラグメントに `blockBehavior` がある場合、終了フラグメントには `blockBehavior: { type: 'end' }` が設定される
|
|
115
|
+
|
|
116
|
+
分割ロジックは式の children 配列で `firstChild !== lastChild` かどうかを確認します。該当する場合:
|
|
117
|
+
|
|
118
|
+
- 式の開始から最初の子の終了までの領域が開始フラグメントになる
|
|
119
|
+
- 最後の子の開始から式の終了までの領域が終了フラグメントになる
|
|
120
|
+
- 間の子は開始フラグメントの psblock 内で通常通り訪問される
|
|
121
|
+
|
|
122
|
+
## 名前空間スコーピング
|
|
123
|
+
|
|
124
|
+
`#updateScopeNS()` プライベートメソッドは、パーサーが要素を走査する際に名前空間コンテキストを管理します:
|
|
125
|
+
|
|
126
|
+
| 条件 | アクション |
|
|
127
|
+
| ----------------------------------------------------- | ---------------------------------------------------- |
|
|
128
|
+
| 現在の名前空間が XHTML で、ノードが `<svg>` 要素 | `scopeNS` を `http://www.w3.org/2000/svg` に切り替え |
|
|
129
|
+
| 現在の名前空間が SVG で、親ノードが `<foreignObject>` | `scopeNS` を `http://www.w3.org/1999/xhtml` に戻す |
|
|
130
|
+
|
|
131
|
+
これは `nodeize()` のノードタイプ switch の前に呼び出されるため、すべての子ノードが正しい名前空間を継承します。名前空間は `visitElement()` 内で `overwriteProps: { namespace: this.state.scopeNS }` を通じて要素に適用されます。
|
|
132
|
+
|
|
133
|
+
名前空間解決の例:
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<div>
|
|
137
|
+
<!-- XHTML -->
|
|
138
|
+
<svg>
|
|
139
|
+
<!-- SVG -->
|
|
140
|
+
<text />
|
|
141
|
+
<!-- SVG -->
|
|
142
|
+
<foreignObject>
|
|
143
|
+
<!-- SVG -->
|
|
144
|
+
<div />
|
|
145
|
+
<!-- XHTML(リセット) -->
|
|
146
|
+
</foreignObject>
|
|
147
|
+
</svg>
|
|
148
|
+
</div>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 属性処理
|
|
152
|
+
|
|
153
|
+
### クォートセット
|
|
154
|
+
|
|
155
|
+
`visitAttr()` メソッドは式の値用に波括弧を含むカスタムクォートセットを使用します:
|
|
156
|
+
|
|
157
|
+
| 開始 | 終了 | タイプ |
|
|
158
|
+
| ---- | ---- | -------- |
|
|
159
|
+
| `"` | `"` | `string` |
|
|
160
|
+
| `'` | `'` | `string` |
|
|
161
|
+
| `{` | `}` | `script` |
|
|
162
|
+
|
|
163
|
+
### ショートハンド属性
|
|
164
|
+
|
|
165
|
+
属性トークンが `{` で始まる場合(例: `{prop}`)、パーサーは `startState: AttrState.BeforeValue` を設定し、名前のパースをスキップして直接値の抽出に進みます。結果の属性は:
|
|
166
|
+
|
|
167
|
+
- `name.raw` = `''`(空)
|
|
168
|
+
- `value.raw` = `prop`
|
|
169
|
+
- `potentialName` = `prop`(値から推論)
|
|
170
|
+
- `isDynamicValue` = `true`
|
|
171
|
+
|
|
172
|
+
### テンプレートディレクティブ
|
|
173
|
+
|
|
174
|
+
Astro テンプレートディレクティブは `name:modifier` 構文を使用します。パーサーは正規表現 `/^([^:]+):([^:]+)$/` でこれらを検出します:
|
|
175
|
+
|
|
176
|
+
| ディレクティブ | `potentialName` | `isDirective` | 動作 |
|
|
177
|
+
| -------------- | --------------- | ------------- | -------------------------------------- |
|
|
178
|
+
| `class:list` | `'class'` | `undefined` | 標準の `class` 属性にマッピング |
|
|
179
|
+
| `set:html` | `undefined` | `true` | Astro 固有ディレクティブとして扱われる |
|
|
180
|
+
| `set:text` | `undefined` | `true` | Astro 固有ディレクティブとして扱われる |
|
|
181
|
+
| `is:raw` | `undefined` | `true` | Astro 固有ディレクティブとして扱われる |
|
|
182
|
+
| `transition:*` | `undefined` | `true` | Astro 固有ディレクティブとして扱われる |
|
|
183
|
+
|
|
184
|
+
`class` ディレクティブは特別で、`potentialName: 'class'` を取得するため、`class` 属性に対する markuplint ルールが適用されます。その他すべてのディレクティブは `isDirective: true` を取得し、フレームワーク固有であり標準 HTML 属性として検証すべきでないことを markuplint に伝えます。
|
|
185
|
+
|
|
186
|
+
### 動的な値
|
|
187
|
+
|
|
188
|
+
開始クォートが `{` の属性はすべて `isDynamicValue: true` を取得します。以下に適用されます:
|
|
189
|
+
|
|
190
|
+
- 明示的な動的値: `prop={value}`
|
|
191
|
+
- ショートハンド属性: `{prop}`
|
|
192
|
+
- ネストされた式: `style={{ a: b }}`
|
|
193
|
+
|
|
194
|
+
## jsx-parser との比較
|
|
195
|
+
|
|
196
|
+
| 機能 | `astro-parser` | `jsx-parser` |
|
|
197
|
+
| ------------------------------ | -------------------------------------- | ------------------------------------------------- |
|
|
198
|
+
| **トークナイザ** | `astro-eslint-parser` | TypeScript ESTree(`@typescript-eslint/parser`) |
|
|
199
|
+
| **フロントマター** | サポート(`---...---` psblock) | 該当なし |
|
|
200
|
+
| **式の構文** | `{expr}` を MustacheTag psblock として | `{expr}` を JSXExpressionContainer psblock として |
|
|
201
|
+
| **テンプレートディレクティブ** | `class:list`、`set:html` 等 | 該当なし |
|
|
202
|
+
| **名前空間管理** | `#updateScopeNS()` で手動管理 | html-parser の `getNamespace()` に委譲 |
|
|
203
|
+
| **コンポーネント検出** | `/^[A-Z]/` パターン | `/^[A-Z]/` パターン |
|
|
204
|
+
| **自己閉じタイプ** | `html+xml` | デフォルト(XML のみ) |
|
|
205
|
+
| **booleanish 属性** | 未設定 | `booleanish: true` |
|
|
206
|
+
| **名前なしフラグメント** | `<>...</>` サポート | `<>...</>` サポート |
|
|
207
|
+
| **スプレッド属性** | 基底パーサーで処理 | カスタム `visitSpreadAttr()` で IDL ルックアップ |
|
|
208
|
+
|
|
209
|
+
## バージョン互換性
|
|
210
|
+
|
|
211
|
+
パースチェーンは以下に依存します:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
astro-eslint-parser → @astrojs/compiler → Astro 構文サポート
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`astro-eslint-parser` は `parseTemplate()` を提供するランタイム依存です。`@astrojs/compiler` は AST 型定義(`Node`、`RootNode`、`ElementNode` 等)にのみ使用される開発依存です。`astro-eslint-parser` を更新する際は、`@astrojs/compiler` 開発依存も `astro-eslint-parser` が内部で使用するバージョンに合わせて更新する必要があります。
|
|
218
|
+
|
|
219
|
+
## 主要ソースファイル
|
|
220
|
+
|
|
221
|
+
| ファイル | 用途 |
|
|
222
|
+
| ----------------- | ------------------------------------------------------------------------------------- |
|
|
223
|
+
| `parser.ts` | `AstroParser` クラス — 全オーバーライドメソッドと名前空間スコーピング |
|
|
224
|
+
| `astro-parser.ts` | `astroParse()` ラッパー — `astro-eslint-parser` に委譲し、診断を `ParserError` に変換 |
|
|
225
|
+
| `index.ts` | 公開 API — シングルトン `parser` インスタンスを再エクスポート |
|
|
226
|
+
|
|
227
|
+
## ドキュメントマップ
|
|
228
|
+
|
|
229
|
+
- [メンテナンスガイド](docs/maintenance.ja.md) -- コマンド、レシピ、トラブルシューティング
|
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# @markuplint/astro-parser
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`@markuplint/astro-parser` is a parser for Astro component files (`.astro`) in markuplint. It uses `astro-eslint-parser` (which wraps `@astrojs/compiler`) to tokenize Astro source code, then converts the resulting AST into markuplint's unified AST format (`MLASTDocument`). The parser handles Astro-specific syntax including frontmatter blocks (`---...---`), expression containers (`{expression}`), template directives (e.g., `class:list`, `set:html`), shorthand attributes (`{prop}`), and namespace-aware element resolution (XHTML vs SVG).
|
|
6
|
+
|
|
7
|
+
## Directory Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
├── index.ts — Re-exports parser instance
|
|
12
|
+
├── parser.ts — AstroParser class extending Parser<Node, State>
|
|
13
|
+
├── astro-parser.ts — astro-eslint-parser wrapper and type re-exports
|
|
14
|
+
├── parser.spec.ts — AstroParser integration tests
|
|
15
|
+
└── astro-parser.spec.ts — astro-eslint-parser wrapper tests
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Architecture Diagram
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
flowchart TD
|
|
22
|
+
subgraph upstream ["Upstream"]
|
|
23
|
+
mlAst["@markuplint/ml-ast\n(AST types)"]
|
|
24
|
+
parserUtils["@markuplint/parser-utils\n(Abstract Parser class)"]
|
|
25
|
+
astroEslintParser["astro-eslint-parser\n(Astro tokenizer)"]
|
|
26
|
+
astroCompiler["@astrojs/compiler\n(AST types)"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
subgraph pkg ["@markuplint/astro-parser"]
|
|
30
|
+
astroParser["AstroParser\nextends Parser‹Node, State›"]
|
|
31
|
+
astroParseFn["astroParse()\nastro-eslint-parser wrapper"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
subgraph downstream ["Downstream"]
|
|
35
|
+
mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
mlAst -->|"AST types"| astroParser
|
|
39
|
+
parserUtils -->|"Parser base class"| astroParser
|
|
40
|
+
astroEslintParser -->|"parseTemplate()"| astroParseFn
|
|
41
|
+
astroCompiler -->|"Node types"| astroParseFn
|
|
42
|
+
astroParseFn -->|"RootNode.children"| astroParser
|
|
43
|
+
astroParser -->|"produces MLASTDocument"| mlCore
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## AstroParser Class
|
|
47
|
+
|
|
48
|
+
### Inheritance
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Parser<Node, State> (from @markuplint/parser-utils)
|
|
52
|
+
└── AstroParser (this package)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Constructor
|
|
56
|
+
|
|
57
|
+
The constructor configures the base `Parser` with Astro-specific options:
|
|
58
|
+
|
|
59
|
+
| Option | Value | Purpose |
|
|
60
|
+
| ---------------------- | ------------ | ---------------------------------------------------------------------------- |
|
|
61
|
+
| `endTagType` | `'xml'` | Astro uses explicit closing tags like XML |
|
|
62
|
+
| `selfCloseType` | `'html+xml'` | Accepts both HTML void elements and XML-style self-closing (`<Component />`) |
|
|
63
|
+
| `tagNameCaseSensitive` | `true` | Distinguishes components (`<MyComp>`) from HTML elements (`<div>`) |
|
|
64
|
+
|
|
65
|
+
### State Type
|
|
66
|
+
|
|
67
|
+
The parser maintains internal state through the `State` type:
|
|
68
|
+
|
|
69
|
+
| Field | Type | Purpose |
|
|
70
|
+
| --------- | -------- | ----------------------------------------------------------------- |
|
|
71
|
+
| `scopeNS` | `string` | Current namespace URI, defaults to `http://www.w3.org/1999/xhtml` |
|
|
72
|
+
|
|
73
|
+
The `scopeNS` state is updated by `#updateScopeNS()` as the parser traverses elements, switching to SVG namespace inside `<svg>` elements and back to XHTML inside `<foreignObject>`.
|
|
74
|
+
|
|
75
|
+
### Override Methods
|
|
76
|
+
|
|
77
|
+
| Method | Purpose |
|
|
78
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| `tokenize()` | Calls `astroParse()` to get the Astro AST, returns `{ ast: rootNode.children, isFragment: true }` |
|
|
80
|
+
| `nodeize()` | Converts Astro AST nodes to markuplint nodes, dispatching by node type (frontmatter, doctype, text, comment, element, expression) |
|
|
81
|
+
| `afterFlattenNodes()` | Delegates to parent with `{ exposeInvalidNode: false }` |
|
|
82
|
+
| `visitElement()` | Parses the raw HTML fragment via `parseCodeFragment()` with `namelessFragment: true`, then delegates to parent with namespace and end tag handling |
|
|
83
|
+
| `visitChildren()` | Delegates to parent, then asserts no unexpected sibling nodes remain |
|
|
84
|
+
| `visitAttr()` | Handles curly-brace expression values, shorthand attributes, and template directives |
|
|
85
|
+
| `detectElementType()` | Detects component vs HTML element using `/^[A-Z]/` pattern (capitalized names are components) |
|
|
86
|
+
|
|
87
|
+
## Frontmatter Handling
|
|
88
|
+
|
|
89
|
+
Astro components can include a frontmatter block delimited by `---`:
|
|
90
|
+
|
|
91
|
+
```astro
|
|
92
|
+
---
|
|
93
|
+
const name = "World";
|
|
94
|
+
---
|
|
95
|
+
<div>{name}</div>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The `astro-eslint-parser` produces a node with `type: 'frontmatter'`. The parser converts this to a **psblock** (pseudo-block) with `nodeName: 'Frontmatter'` and `isFragment: false`. The entire `---...---` block including delimiters is captured as a single opaque node. Content inside the frontmatter is not parsed as HTML.
|
|
99
|
+
|
|
100
|
+
## Expression Handling
|
|
101
|
+
|
|
102
|
+
Astro expressions (`{expression}`) are represented as `type: 'expression'` nodes in the Astro AST. The parser converts these to **MustacheTag** psblock nodes.
|
|
103
|
+
|
|
104
|
+
### Simple Expressions
|
|
105
|
+
|
|
106
|
+
A simple expression like `{name}` has a single text child. The entire expression is emitted as one MustacheTag psblock with `isFragment: true`.
|
|
107
|
+
|
|
108
|
+
### Nested Expressions with HTML
|
|
109
|
+
|
|
110
|
+
When an expression contains HTML elements (e.g., `{list.map(item => <li>{item}</li>)}`), the parser splits it into multiple nodes:
|
|
111
|
+
|
|
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
|
+
2. **Nested HTML elements**: `<li>{item}</li>` — processed as normal elements
|
|
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
|
+
|
|
116
|
+
The splitting logic checks whether `firstChild !== lastChild` in the expression's children array. If so:
|
|
117
|
+
|
|
118
|
+
- The region from the expression start to the first child's end becomes the opening fragment
|
|
119
|
+
- The region from the last child's start to the expression end becomes the closing fragment
|
|
120
|
+
- The children between are visited normally within the opening fragment's psblock
|
|
121
|
+
|
|
122
|
+
## Namespace Scoping
|
|
123
|
+
|
|
124
|
+
The `#updateScopeNS()` private method manages namespace context as the parser traverses elements:
|
|
125
|
+
|
|
126
|
+
| Condition | Action |
|
|
127
|
+
| ------------------------------------------------------------- | ------------------------------------------------------- |
|
|
128
|
+
| Current namespace is XHTML and node is `<svg>` element | Switch `scopeNS` to `http://www.w3.org/2000/svg` |
|
|
129
|
+
| Current namespace is SVG and parent node is `<foreignObject>` | Switch `scopeNS` back to `http://www.w3.org/1999/xhtml` |
|
|
130
|
+
|
|
131
|
+
This is called at the beginning of `nodeize()` before the node type switch, so all child nodes inherit the correct namespace. The namespace is applied to elements via `overwriteProps: { namespace: this.state.scopeNS }` in `visitElement()`.
|
|
132
|
+
|
|
133
|
+
Example namespace resolution:
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<div>
|
|
137
|
+
<!-- XHTML -->
|
|
138
|
+
<svg>
|
|
139
|
+
<!-- SVG -->
|
|
140
|
+
<text />
|
|
141
|
+
<!-- SVG -->
|
|
142
|
+
<foreignObject>
|
|
143
|
+
<!-- SVG -->
|
|
144
|
+
<div />
|
|
145
|
+
<!-- XHTML (reset) -->
|
|
146
|
+
</foreignObject>
|
|
147
|
+
</svg>
|
|
148
|
+
</div>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Attribute Processing
|
|
152
|
+
|
|
153
|
+
### Quote Set
|
|
154
|
+
|
|
155
|
+
The `visitAttr()` method uses a custom quote set that includes curly braces for expression values:
|
|
156
|
+
|
|
157
|
+
| Start | End | Type |
|
|
158
|
+
| ----- | --- | -------- |
|
|
159
|
+
| `"` | `"` | `string` |
|
|
160
|
+
| `'` | `'` | `string` |
|
|
161
|
+
| `{` | `}` | `script` |
|
|
162
|
+
|
|
163
|
+
### Shorthand Attributes
|
|
164
|
+
|
|
165
|
+
When an attribute token starts with `{` (e.g., `{prop}`), the parser sets `startState: AttrState.BeforeValue`, which skips name parsing and goes directly to value extraction. The resulting attribute has:
|
|
166
|
+
|
|
167
|
+
- `name.raw` = `''` (empty)
|
|
168
|
+
- `value.raw` = `prop`
|
|
169
|
+
- `potentialName` = `prop` (inferred from value)
|
|
170
|
+
- `isDynamicValue` = `true`
|
|
171
|
+
|
|
172
|
+
### Template Directives
|
|
173
|
+
|
|
174
|
+
Astro template directives use the `name:modifier` syntax. The parser detects these with the regex `/^([^:]+):([^:]+)$/`:
|
|
175
|
+
|
|
176
|
+
| Directive | `potentialName` | `isDirective` | Behavior |
|
|
177
|
+
| -------------- | --------------- | ------------- | ----------------------------------- |
|
|
178
|
+
| `class:list` | `'class'` | `undefined` | Maps to standard `class` attribute |
|
|
179
|
+
| `set:html` | `undefined` | `true` | Treated as Astro-specific directive |
|
|
180
|
+
| `set:text` | `undefined` | `true` | Treated as Astro-specific directive |
|
|
181
|
+
| `is:raw` | `undefined` | `true` | Treated as Astro-specific directive |
|
|
182
|
+
| `transition:*` | `undefined` | `true` | Treated as Astro-specific directive |
|
|
183
|
+
|
|
184
|
+
The `class` directive is special: it gets `potentialName: 'class'` so markuplint rules for the `class` attribute apply. All other directives get `isDirective: true`, which tells markuplint they are framework-specific and should not be validated as standard HTML attributes.
|
|
185
|
+
|
|
186
|
+
### Dynamic Values
|
|
187
|
+
|
|
188
|
+
Any attribute whose start quote is `{` gets `isDynamicValue: true`. This applies to:
|
|
189
|
+
|
|
190
|
+
- Explicit dynamic values: `prop={value}`
|
|
191
|
+
- Shorthand attributes: `{prop}`
|
|
192
|
+
- Nested expressions: `style={{ a: b }}`
|
|
193
|
+
|
|
194
|
+
## Comparison with jsx-parser
|
|
195
|
+
|
|
196
|
+
| Feature | `astro-parser` | `jsx-parser` |
|
|
197
|
+
| ------------------------- | ------------------------------- | ----------------------------------------------- |
|
|
198
|
+
| **Tokenizer** | `astro-eslint-parser` | TypeScript ESTree (`@typescript-eslint/parser`) |
|
|
199
|
+
| **Frontmatter** | Supported (`---...---` psblock) | Not applicable |
|
|
200
|
+
| **Expression syntax** | `{expr}` as MustacheTag psblock | `{expr}` as JSXExpressionContainer psblock |
|
|
201
|
+
| **Template directives** | `class:list`, `set:html`, etc. | Not applicable |
|
|
202
|
+
| **Namespace management** | Manual via `#updateScopeNS()` | Delegates to `getNamespace()` from html-parser |
|
|
203
|
+
| **Component detection** | `/^[A-Z]/` pattern | `/^[A-Z]/` pattern |
|
|
204
|
+
| **Self-close type** | `html+xml` | Default (XML-only) |
|
|
205
|
+
| **Booleanish attributes** | Not configured | `booleanish: true` |
|
|
206
|
+
| **Nameless fragments** | `<>...</>` supported | `<>...</>` supported |
|
|
207
|
+
| **Spread attributes** | Handled by base parser | Custom `visitSpreadAttr()` with IDL lookup |
|
|
208
|
+
|
|
209
|
+
## Version Compatibility
|
|
210
|
+
|
|
211
|
+
The parsing chain depends on:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
astro-eslint-parser → @astrojs/compiler → Astro syntax support
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`astro-eslint-parser` is a runtime dependency that provides `parseTemplate()`. `@astrojs/compiler` is a dev dependency used only for AST type definitions (`Node`, `RootNode`, `ElementNode`, etc.). When updating `astro-eslint-parser`, the `@astrojs/compiler` dev dependency should also be updated to match the version that `astro-eslint-parser` uses internally.
|
|
218
|
+
|
|
219
|
+
## Key Source Files
|
|
220
|
+
|
|
221
|
+
| File | Purpose |
|
|
222
|
+
| ----------------- | -------------------------------------------------------------------------------------------------- |
|
|
223
|
+
| `parser.ts` | `AstroParser` class — all override methods and namespace scoping |
|
|
224
|
+
| `astro-parser.ts` | `astroParse()` wrapper — delegates to `astro-eslint-parser`, converts diagnostics to `ParserError` |
|
|
225
|
+
| `index.ts` | Public API — re-exports the singleton `parser` instance |
|
|
226
|
+
|
|
227
|
+
## Documentation Map
|
|
228
|
+
|
|
229
|
+
- [Maintenance Guide](docs/maintenance.md) -- Commands, recipes, and troubleshooting
|
package/CHANGELOG.md
CHANGED
|
@@ -3,13 +3,37 @@
|
|
|
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
|
-
|
|
6
|
+
# [5.0.0-alpha.0](https://github.com/markuplint/markuplint/compare/v4.14.1...v5.0.0-alpha.0) (2026-02-20)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **ml-core:** improve detection of namespace ([5b507ad](https://github.com/markuplint/markuplint/commit/5b507ad7c19c5015b8ce587845d901e31dfa6518))
|
|
11
|
+
|
|
12
|
+
- refactor(astro-parser)!: update for simplified AST token properties ([4c05de1](https://github.com/markuplint/markuplint/commit/4c05de151d30233a8d4a184c4cb70c26de19b36b))
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- **astro-parser:** support loop blocks ([ebe2eb6](https://github.com/markuplint/markuplint/commit/ebe2eb6b85aa32ff3f29964e333d058afe99d18b))
|
|
9
17
|
|
|
18
|
+
### BREAKING CHANGES
|
|
10
19
|
|
|
20
|
+
- Adapt to renamed token properties and remove
|
|
21
|
+
selfClosingSolidus test.
|
|
11
22
|
|
|
23
|
+
* Token property access: startOffset -> offset, startLine -> line,
|
|
24
|
+
startCol -> col
|
|
25
|
+
* Replace selfClosingSolidus check with tagCloseChar
|
|
26
|
+
* Remove selfClosingSolidus test case
|
|
12
27
|
|
|
28
|
+
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
29
|
+
|
|
30
|
+
## [4.6.23](https://github.com/markuplint/markuplint/compare/@markuplint/astro-parser@4.6.22...@markuplint/astro-parser@4.6.23) (2026-02-10)
|
|
31
|
+
|
|
32
|
+
**Note:** Version bump only for package @markuplint/astro-parser
|
|
33
|
+
|
|
34
|
+
## [4.6.22](https://github.com/markuplint/markuplint/compare/@markuplint/astro-parser@4.6.21...@markuplint/astro-parser@4.6.22) (2025-11-05)
|
|
35
|
+
|
|
36
|
+
**Note:** Version bump only for package @markuplint/astro-parser
|
|
13
37
|
|
|
14
38
|
## [4.6.21](https://github.com/markuplint/markuplint/compare/@markuplint/astro-parser@4.6.20...@markuplint/astro-parser@4.6.21) (2025-08-24)
|
|
15
39
|
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Maintenance tasks for @markuplint/astro-parser
|
|
3
|
+
globs:
|
|
4
|
+
- packages/@markuplint/astro-parser/src/**/*.ts
|
|
5
|
+
alwaysApply: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# astro-parser-maintenance
|
|
9
|
+
|
|
10
|
+
Perform maintenance tasks for `@markuplint/astro-parser`: add template directives,
|
|
11
|
+
modify namespace scoping, update expression handling, and manage astro-eslint-parser integration.
|
|
12
|
+
|
|
13
|
+
## Input
|
|
14
|
+
|
|
15
|
+
`$ARGUMENTS` specifies the task. Supported tasks:
|
|
16
|
+
|
|
17
|
+
| Task | Description |
|
|
18
|
+
| ---------------------------- | --------------------------------------------------- |
|
|
19
|
+
| `add-directive` | Add a new Astro template directive |
|
|
20
|
+
| `modify-namespace-scoping` | Modify SVG/XHTML namespace scoping logic |
|
|
21
|
+
| `update-expression-handling` | Update expression splitting or MustacheTag handling |
|
|
22
|
+
|
|
23
|
+
If omitted, defaults to `add-directive`.
|
|
24
|
+
|
|
25
|
+
## Reference
|
|
26
|
+
|
|
27
|
+
Before executing any task, read `docs/maintenance.md` (or `docs/maintenance.ja.md`)
|
|
28
|
+
for the full guide. The recipes there are the source of truth for procedures.
|
|
29
|
+
|
|
30
|
+
Also read:
|
|
31
|
+
|
|
32
|
+
- `ARCHITECTURE.md` -- Package overview, attribute processing, namespace scoping
|
|
33
|
+
- `src/parser.ts` -- AstroParser class (source of truth for override methods)
|
|
34
|
+
- `src/astro-parser.ts` -- astro-eslint-parser wrapper
|
|
35
|
+
|
|
36
|
+
## Task: add-directive
|
|
37
|
+
|
|
38
|
+
Add a new Astro template directive. Follow recipe #1 in `docs/maintenance.md`.
|
|
39
|
+
|
|
40
|
+
### Step 1: Understand the directive pattern
|
|
41
|
+
|
|
42
|
+
1. Read `src/parser.ts` — the `visitAttr()` method
|
|
43
|
+
2. Identify the regex: `/^([^:]+):([^:]+)$/`
|
|
44
|
+
3. Understand the `switch (lowerCaseDirectiveName)` block
|
|
45
|
+
|
|
46
|
+
### Step 2: Add the directive case
|
|
47
|
+
|
|
48
|
+
1. Add a new `case` in the switch for the directive prefix
|
|
49
|
+
2. Decide whether it maps to a `potentialName` (like `class:list` → `class`) or is a pure directive (`isDirective: true`)
|
|
50
|
+
3. If it maps to a standard HTML attribute, set `potentialName` to the attribute name
|
|
51
|
+
4. If it is Astro-specific, set `isDirective = true`
|
|
52
|
+
|
|
53
|
+
### Step 3: Verify
|
|
54
|
+
|
|
55
|
+
1. Build: `yarn build --scope @markuplint/astro-parser`
|
|
56
|
+
2. Add test cases to `src/parser.spec.ts` using `nodeListToDebugMaps`
|
|
57
|
+
3. Test: `yarn test --scope @markuplint/astro-parser`
|
|
58
|
+
|
|
59
|
+
## Task: modify-namespace-scoping
|
|
60
|
+
|
|
61
|
+
Modify the SVG/XHTML namespace scoping logic. Follow recipe #2 in `docs/maintenance.md`.
|
|
62
|
+
|
|
63
|
+
### Step 1: Understand the current logic
|
|
64
|
+
|
|
65
|
+
1. Read `src/parser.ts` — the `#updateScopeNS()` private method
|
|
66
|
+
2. Understand the two conditions: `<svg>` → SVG namespace, `<foreignObject>` parent → XHTML namespace
|
|
67
|
+
3. Note that `scopeNS` is applied in `visitElement()` via `overwriteProps`
|
|
68
|
+
|
|
69
|
+
### Step 2: Make the change
|
|
70
|
+
|
|
71
|
+
1. Add or modify conditions in `#updateScopeNS()`
|
|
72
|
+
2. For new namespaces (e.g., MathML), add a new condition checking `originNode.name`
|
|
73
|
+
3. Ensure the namespace URI constant is correct
|
|
74
|
+
|
|
75
|
+
### Step 3: Verify
|
|
76
|
+
|
|
77
|
+
1. Build: `yarn build --scope @markuplint/astro-parser`
|
|
78
|
+
2. Add namespace test cases to `src/parser.spec.ts`
|
|
79
|
+
3. Test: `yarn test --scope @markuplint/astro-parser`
|
|
80
|
+
|
|
81
|
+
## Task: update-expression-handling
|
|
82
|
+
|
|
83
|
+
Update expression splitting or MustacheTag handling. Follow recipe #3 in `docs/maintenance.md`.
|
|
84
|
+
|
|
85
|
+
### Step 1: Understand the current logic
|
|
86
|
+
|
|
87
|
+
1. Read `src/parser.ts` — the `case 'expression'` block in `nodeize()`
|
|
88
|
+
2. Understand the splitting logic: `firstChild !== lastChild` check
|
|
89
|
+
3. Understand how opening and closing fragments are created
|
|
90
|
+
|
|
91
|
+
### Step 2: Make the change
|
|
92
|
+
|
|
93
|
+
1. Modify the splitting logic in the `expression` case
|
|
94
|
+
2. Ensure `startExpressionRaw` and `startExpressionStartLine`/`startExpressionStartCol` are correctly set
|
|
95
|
+
3. Ensure the closing fragment location is correctly calculated from `lastChild`
|
|
96
|
+
|
|
97
|
+
### Step 3: Verify
|
|
98
|
+
|
|
99
|
+
1. Build: `yarn build --scope @markuplint/astro-parser`
|
|
100
|
+
2. Test with expressions containing nested HTML (e.g., `{list.map(item => <li>{item}</li>)}`)
|
|
101
|
+
3. Test: `yarn test --scope @markuplint/astro-parser`
|
|
102
|
+
|
|
103
|
+
## Rules
|
|
104
|
+
|
|
105
|
+
1. **Delegate tokenization to astro-eslint-parser** — never parse Astro syntax manually; always use `astroParse()`.
|
|
106
|
+
2. **Use `potentialName` for attribute mapping** — when a directive maps to a standard HTML attribute, set `potentialName` instead of modifying the attribute name.
|
|
107
|
+
3. **Test with `nodeListToDebugMaps`** — all parser tests should use `nodeListToDebugMaps` for snapshot-style assertions that verify positions, names, and types.
|
|
108
|
+
4. **Maintain `scopeNS` state** — namespace scoping must be updated before node type dispatch in `nodeize()`.
|
|
109
|
+
5. **Add JSDoc comments** to all new public methods and properties.
|