@markuplint/vue-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.
@@ -0,0 +1,302 @@
1
+ # @markuplint/vue-parser
2
+
3
+ ## 概要
4
+
5
+ `@markuplint/vue-parser` は markuplint 用の Vue Single File Component(SFC)テンプレートパーサーです。vue-eslint-parser を使用して Vue SFC の `<template>` ブロックを vue-eslint-parser AST にパースし、その後統一された markuplint AST 形式(`MLASTDocument`)に変換します。Vue 固有のディレクティブ(`v-bind`、`v-on`、`v-model`、`v-slot`)、テンプレート式コンテナ(`{{ }}`)、テンプレートコメント、PascalCase コンポーネント検出を処理します。
6
+
7
+ ## ディレクトリ構成
8
+
9
+ ```
10
+ src/
11
+ ├── index.ts — parser を再エクスポート
12
+ ├── parser.ts — Parser<ASTNode, State> を拡張する VueParser クラス
13
+ ├── index.spec.ts — VueParser の統合テスト
14
+ └── vue-parser/
15
+ └── index.ts — vue-eslint-parser ラッパー、ASTNode/ASTComment 型エクスポート
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
+ vueEslintParser["vue-eslint-parser\n(Vue SFC トークナイザ)"]
26
+ end
27
+
28
+ subgraph pkg ["@markuplint/vue-parser"]
29
+ vueParser["VueParser\nextends Parser‹ASTNode, State›"]
30
+ vueParseFn["vueParse()\nvue-eslint-parser ラッパー"]
31
+ end
32
+
33
+ subgraph downstream ["下流"]
34
+ mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
35
+ end
36
+
37
+ mlAst -->|"AST 型"| vueParser
38
+ parserUtils -->|"Parser 基底クラス"| vueParser
39
+ vueEslintParser -->|"parse()"| vueParseFn
40
+ vueParseFn -->|"ESLintProgram AST"| vueParser
41
+
42
+ vueParser -->|"MLASTDocument を生成"| mlCore
43
+ ```
44
+
45
+ ## VueParser クラス
46
+
47
+ ### 継承関係
48
+
49
+ ```
50
+ Parser<ASTNode, State> (@markuplint/parser-utils)
51
+ └── VueParser (このパッケージ)
52
+ ```
53
+
54
+ ### コンストラクタ
55
+
56
+ コンストラクタは2つの引数でパーサーを構成します:
57
+
58
+ | 引数 | 値 | 用途 |
59
+ | --------------- | ----------------------- | ---------------------------------------------------------------------------------- |
60
+ | `ParserOptions` | `{ endTagType: 'xml' }` | Vue テンプレートは明示的な閉じタグを使用(XML スタイル)、HTML void ルールではない |
61
+ | 初期 State | `{ comments: [] }` | 空のコメント配列。`tokenize()` で設定される |
62
+
63
+ `tagNameCaseSensitive` の動作は基底クラスから継承され、Vue の `detectElementType` オーバーライドと組み合わせて PascalCase コンポーネント名を正しく処理します。
64
+
65
+ ### State 型
66
+
67
+ パーサーは `State` 型を通じて内部状態を管理します:
68
+
69
+ | フィールド | 型 | 用途 |
70
+ | ---------- | ----------------------- | -------------------------------------------------------------------------------------------- |
71
+ | `comments` | `readonly ASTComment[]` | tokenize 中に vue-eslint-parser から抽出されたテンプレートコメント。後の flattenNodes で注入 |
72
+
73
+ ### オーバーライドメソッド
74
+
75
+ | メソッド | 用途 |
76
+ | --------------------- | --------------------------------------------------------------------------------------------- |
77
+ | `tokenize()` | vue-eslint-parser を呼び出し、`templateBody.children` とコメントを抽出 |
78
+ | `parseError()` | vue-eslint-parser の `SyntaxError`(`lineNumber`/`column` 付き)を `ParserError` に変換 |
79
+ | `nodeize()` | vue-eslint-parser AST ノード(VText、VElement、VExpressionContainer)を markuplint AST に変換 |
80
+ | `flattenNodes()` | 基底のフラット化を拡張し、兄弟ノード間にテンプレートコメントを注入 |
81
+ | `afterFlattenNodes()` | `exposeWhiteSpace: false`、`exposeInvalidNode: false`、`concatText: false` で基底を呼び出す |
82
+ | `detectElementType()` | PascalCase コンポーネントと Vue 組み込みコンポーネントを検出 |
83
+
84
+ > **注記:** `visitAttr()` オーバーライドは削除されました。Vue ディレクティブ処理(`v-bind`、`v-on`、`v-model`、`v-slot` など)は `@markuplint/vue-spec` の `directivePatterns` で管理されています。
85
+
86
+ ### `duplicatableAttrs`
87
+
88
+ `'class'` と `'style'` を含む `Set<string>` -- `v-bind:class` と `class` が同一要素に共存できるように、重複可能な属性を定義します。
89
+
90
+ ## tokenize()
91
+
92
+ `tokenize()` メソッドは vue-eslint-parser AST を取得するエントリーポイントです:
93
+
94
+ 1. `vueParse(this.rawCode)` を呼び出し、内部で `VueESLintParser.parse(vueTemplate, { parser: false })` を実行
95
+ 2. `ast.templateBody?.comments` が存在する場合、`this.state.comments` に格納して後の注入に備える
96
+ 3. `{ ast: ast.templateBody?.children ?? [], isFragment: true }` を返す
97
+
98
+ `parser: false` オプションは vue-eslint-parser に `<script>` のパースをスキップするよう指示します(markuplint にとって関連するのは `<template>` ブロックのみ)。ソースに `<template>` ブロックがないか空の場合、`templateBody?.children` は `undefined` を返し、パーサーは空の配列を受け取ります。
99
+
100
+ ## nodeize() の詳細
101
+
102
+ `nodeize()` メソッドは `originNode.type` フィールドに基づいてディスパッチします:
103
+
104
+ ### VText -> visitText
105
+
106
+ テキストノードは `this.sliceFragment(range[0], range[1])` でソースからスライスされ、depth と parentNode と共に基底の `visitText()` メソッドに渡されます。
107
+
108
+ ### VExpressionContainer -> visitPsBlock
109
+
110
+ `{{ expression }}` のような式コンテナは `visitPsBlock()` で擬似ブロックノードに変換されます:
111
+
112
+ - `nodeName`: `'vue-expression-container'`
113
+ - `isFragment`: `false`
114
+
115
+ これにより、Vue テンプレート式は JavaScript コンテンツのパースを試みるのではなく、markuplint AST 内で不透明なブロックとして扱われます。
116
+
117
+ ### VElement -> visitElement
118
+
119
+ 要素ノードの場合、メソッドは:
120
+
121
+ 1. `originNode.startTag.range` から**開始タグ**トークンをスライス
122
+ 2. 要素の `name` と `namespace` と共に `visitElement()` を呼び出す
123
+ 3. `originNode.children` を子ノードとして渡す -- テンプレートルートを表すノードの場合、これは `templateBody.children`
124
+ 4. エンドタグトークンファクトリ(`createEndTagToken`)を作成 -- 要素が自己閉じの場合は `null` を返し、そうでなければ `originNode.endTag.range` からスライス
125
+
126
+ ## flattenNodes()
127
+
128
+ `flattenNodes()` メソッドは基底の `Parser.flattenNodes()` を拡張してテンプレートコメントを注入します:
129
+
130
+ 1. `super.flattenNodes(nodeTree)` を呼び出して初期フラットノードリストを取得
131
+ 2. ノードリストを走査し、隣接するノードペア間のコメントを確認
132
+ 3. `prevNode.endOffset`(最初のノードの場合は `parentNode.endOffset`)と `node.startOffset` の間の各ギャップについて、そのギャップ内に範囲が収まるコメントを `this.state.comments` から検索
133
+ 4. コメントが見つかった場合、`this.visitComment()` で作成し、コメントの type に基づいて `isBogus` を設定(`HTMLBogusComment` の場合は true)
134
+ 5. `this.appendChild()` でコメントを親ノードに追加
135
+
136
+ この2パスアプローチが必要な理由は、vue-eslint-parser がコメントをメインノードツリーとは別に提供するため、正しい位置にインターリーブする必要があるからです。
137
+
138
+ ## ディレクティブ処理(@markuplint/vue-spec の directivePatterns)
139
+
140
+ Vue ディレクティブの解決はパーサー自体ではなく、`@markuplint/vue-spec` で定義された `directivePatterns` によって管理されています。spec がディレクティブを `potentialName`、`isDirective`、`isDynamicValue` メタデータにマッピングするパターンを宣言します。
141
+
142
+ > **二段階解決:** パーサーレベルのテスト(`index.spec.ts`)はパーサー自体が設定する raw AST 値を示します(例: 波括弧式のみ `isDynamicValue: true`)。コアレベルのテスト(`ml-core` や `rules`)は `ml-core` の `MLAttr` コンストラクタが `directivePatterns` を適用した後の最終解決値を示します。例えば、値なしの `on:click` はパーサーレベルでは `isDynamicValue: false` ですが、コアレベルでは `directivePatterns` マッチにより `isDynamicValue: true` に解決されます。
143
+
144
+ ### クォートセット
145
+
146
+ 基底パーサーは標準 HTML クォート(`"`、`'`)を処理します。Vue テンプレートは式バインディングの暗黙的な値デリミタとして `{}` も使用しますが、属性値自体は標準のクォーティングを使用します。
147
+
148
+ ### Vue ディレクティブ処理
149
+
150
+ ディレクティブは優先順位に従って処理されます。最初にマッチするパターンが適用されます:
151
+
152
+ #### `v-on` / `@`(イベントバインディング)
153
+
154
+ - **パターン**: `/^(v-on:|@)([^.]+)(?:\.([^.]+))?$/i`
155
+ - **結果**: `potentialName: 'on' + eventName.toLowerCase()`、`isDynamicValue: true`
156
+ - **例**:
157
+ - `@click` -> `potentialName: 'onclick'`
158
+ - `v-on:click.stop` -> `potentialName: 'onclick'`
159
+ - `@keydown.enter` -> `potentialName: 'onkeydown'`
160
+
161
+ #### `v-bind` / `:`(プロパティバインディング)
162
+
163
+ - **パターン**: `/^(v-bind:|:)([^.]+)(?:\.([^.]+))?$/i`
164
+ - **結果**(修飾子なし): `potentialName: propName`、`isDynamicValue: true`
165
+ - **結果**(`.attr` 修飾子): `potentialName: propName`、`isDynamicValue: true`
166
+ - **結果**(`.prop` / `.camel` / その他修飾子): `isDirective: true`、`potentialName` が正規化された形式に設定
167
+ - **`isDuplicatable`**: バインドされたプロパティが `duplicatableAttrs` に含まれる場合(class、style)、`isDuplicatable` が `true` に設定
168
+ - **例**:
169
+ - `:data-attr` -> `potentialName: 'data-attr'`
170
+ - `v-bind:class` -> `potentialName: 'class'`、`isDuplicatable: true`
171
+ - `:title.attr` -> `potentialName: 'title'`
172
+ - `:foo.prop` -> `isDirective: true`
173
+
174
+ #### `v-model`
175
+
176
+ - **パターン**: `/^(v-model)(?:\.([^.]+))?$/i`
177
+ - **結果**: `isDirective: true`
178
+ - **例**:
179
+ - `v-model` -> `isDirective: true`
180
+ - `v-model.lazy` -> `isDirective: true`
181
+
182
+ #### `v-slot` / `#`(スロット)
183
+
184
+ - **パターン**: `/^(v-slot:|#)(.+)$/i`
185
+ - **結果**: `isDirective: true`、`potentialName: 'v-slot:' + slotName`(raw name と異なる場合)
186
+ - **例**:
187
+ - `#header` -> `potentialName: 'v-slot:header'`、`isDirective: true`
188
+ - `v-slot:default` -> `isDirective: true`
189
+
190
+ #### その他の `v-` ディレクティブ
191
+
192
+ - **パターン**: `v-` で始まる
193
+ - **結果**: `isDirective: true`
194
+ - **例**: `v-if`、`v-for`、`v-show`、`v-else`、`v-else-if`、`v-pre`、`v-cloak`、`v-once`、`v-memo`、`v-html`、`v-text`
195
+
196
+ ## 要素タイプ検出
197
+
198
+ `detectElementType()` メソッドは Vue 固有のコンポーネント検出のためのマッチャー配列を使って `super.detectElementType(nodeName, matchers)` を呼び出します:
199
+
200
+ | マッチャー | 型 | マッチ対象 |
201
+ | ------------------- | ------ | ----------------------------------------------- |
202
+ | `'Transition'` | String | Vue 組み込み `<Transition>` コンポーネント |
203
+ | `'TransitionGroup'` | String | Vue 組み込み `<TransitionGroup>` コンポーネント |
204
+ | `'KeepAlive'` | String | Vue 組み込み `<KeepAlive>` コンポーネント |
205
+ | `'Teleport'` | String | Vue 組み込み `<Teleport>` コンポーネント |
206
+ | `'Suspense'` | String | Vue 組み込み `<Suspense>` コンポーネント |
207
+ | `'component'` | String | Vue 特殊要素 `<component :is="...">` |
208
+ | `'slot'` | String | Vue 特殊要素 `<slot>` |
209
+ | `/^[A-Z]/` | RegExp | PascalCase のタグ名(ユーザーコンポーネント) |
210
+
211
+ タグ名がこれらのいずれかにマッチする場合、`detectElementType()` は `'authored'`(コンポーネントを示す)を返します。それ以外は標準の HTML 要素検出が適用されます:
212
+
213
+ - `div`、`span`、`p` 等 -> `'html'`
214
+ - `x-foo`、`my-element` -> `'web-component'`
215
+
216
+ `<transition>`(小文字)は組み込みリストにマッチ**しない**ため標準 HTML 要素(`'html'`)として扱われますが、`<Transition>`(PascalCase)は `'authored'` として扱われることに注意してください。
217
+
218
+ ## afterFlattenNodes()
219
+
220
+ `afterFlattenNodes()` メソッドは特定のオプションで基底実装を呼び出します:
221
+
222
+ | オプション | 値 | 効果 |
223
+ | ------------------- | ------- | -------------------------------------------------------- |
224
+ | `exposeWhiteSpace` | `false` | 空白のみのテキストノードは別の無効ノードとして公開しない |
225
+ | `exposeInvalidNode` | `false` | 無効なノードは公開しない |
226
+ | `concatText` | `false` | 隣接するテキストノードは結合しない |
227
+
228
+ これらの設定は、Vue のテンプレートパーサーが空白やノードの妥当性を生の HTML パースとは異なる方法で処理することを反映しています。
229
+
230
+ ## バージョン互換性
231
+
232
+ vue-eslint-parser 依存は Vue 2 と Vue 3 の両方のテンプレート構文をサポートしています。パーサーは AST レベルで Vue バージョンを区別しません -- どちらも同じ `VElement`、`VText`、`VExpressionContainer` ノードタイプを生成します。Vue 3 固有の機能(`<Teleport>` や `<Suspense>` など)はパーサーレベルの変更ではなく、要素タイプ検出を通じて処理されます。
233
+
234
+ ## 制約事項
235
+
236
+ ### `v-if` / `v-for` の `blockBehavior` 未対応
237
+
238
+ 他のフレームワークパーサー(Svelte、Pug、Alpine、JSX、Astro)は条件分岐/ループ構文に `blockBehavior` を設定し、コアエンジンが `conditionalChildNodes()` を通じて全てのありうる子ノードパターンを列挙できるようにしています。Vue パーサーはこれを**サポートしていません**。そのため、`permitted-contents` などのルールは Vue テンプレートの `v-if`/`v-else` 分岐や `v-for` イテレーション全体のコンテンツモデル検証ができません。
239
+
240
+ **実装が困難な理由:**
241
+
242
+ Alpine.js では条件分岐とループに決まったパターン — `<template x-for="...">` / `<template x-if="...">` — を使用しており、`<template>` 要素をそのまま PSBlock に変換できます。Vue のディレクティブは根本的に異なる仕組みで動作します:
243
+
244
+ 1. **ディレクティブは任意の要素に付与可能**: `v-if`、`v-for`、`v-else`、`v-else-if` は任意の要素に配置できます(例: `<div v-if="...">`、`<li v-for="...">`)。要素は属性検証のために有効な HTML 要素として残しつつ、コンテンツモデル解析のためにブロックとしても機能させる必要があります — この二重の役割は現在のパーサーアーキテクチャではサポートされていません。
245
+
246
+ 2. **兄弟要素ベースの分岐**: `v-else` と `v-else-if` はラッパーブロックの子構造ではなく、**兄弟**要素の属性です。条件グループの構築には、現在のノード単位の `nodeize()` モデルを超えた兄弟間解析が必要です。
247
+
248
+ 現在の Vue パーサーはこれらのディレクティブを属性レベルでのみ処理しており(`isDirective: true`)、属性検証エラーは抑制されますが、構造的なブロック情報はコアエンジンに提供されません。
249
+
250
+ ## 主要ソースファイル
251
+
252
+ | ファイル | 用途 |
253
+ | ------------------------- | ----------------------------------------------------------------- |
254
+ | `src/parser.ts` | 全オーバーライドメソッドを持つ VueParser クラス |
255
+ | `src/vue-parser/index.ts` | vue-eslint-parser ラッパーと型定義(ASTNode、ASTComment) |
256
+ | `src/index.ts` | モジュールエントリーポイント、parser インスタンスを再エクスポート |
257
+ | `src/index.spec.ts` | パース、ディレクティブ、名前空間をカバーする統合テスト |
258
+
259
+ ## 外部依存
260
+
261
+ | 依存パッケージ | 用途 |
262
+ | -------------------------- | ---------------------------------------------------------------- |
263
+ | `@markuplint/ml-ast` | AST 型定義(`MLASTParentNode`、`MLASTNodeTreeItem` 等) |
264
+ | `@markuplint/parser-utils` | 抽象 `Parser` クラス、`ParserError`、`Token`、`ChildToken` |
265
+ | `@markuplint/html-parser` | ピア依存(直接インポートされないが、パーサーエコシステムの一部) |
266
+ | `vue-eslint-parser` | Vue SFC テンプレートパース(`parse`、AST 型) |
267
+
268
+ ## 統合ポイント
269
+
270
+ ```mermaid
271
+ flowchart TD
272
+ subgraph upstream ["上流"]
273
+ mlAst["@markuplint/ml-ast\n(AST 型定義)"]
274
+ parserUtils["@markuplint/parser-utils\n(Parser 基底クラス)"]
275
+ vueEslintParser["vue-eslint-parser\n(Vue SFC トークナイザ)"]
276
+ end
277
+
278
+ subgraph pkg ["@markuplint/vue-parser"]
279
+ vueParser["VueParser"]
280
+ end
281
+
282
+ subgraph downstream ["下流"]
283
+ mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
284
+ end
285
+
286
+ upstream -->|"型、パース"| vueParser
287
+ vueParser -->|"MLASTDocument を生成"| mlCore
288
+ ```
289
+
290
+ ### 上流
291
+
292
+ - **`@markuplint/ml-ast`** -- パーサー全体で使用される AST 型定義
293
+ - **`@markuplint/parser-utils`** -- `VueParser` が拡張する抽象 `Parser` クラスと `ParserError` およびユーティリティ型
294
+ - **`vue-eslint-parser`** -- テンプレートのトークン化とツリー構築を行う基盤 Vue SFC パーサー
295
+
296
+ ### 下流
297
+
298
+ - **`@markuplint/ml-core`** -- `VueParser` が生成する `MLASTDocument` を消費し、ルール評価のための MLDOM を構築
299
+
300
+ ## ドキュメントマップ
301
+
302
+ - [メンテナンスガイド](docs/maintenance.ja.md) -- コマンド、レシピ、トラブルシューティング
@@ -0,0 +1,302 @@
1
+ # @markuplint/vue-parser
2
+
3
+ ## Overview
4
+
5
+ `@markuplint/vue-parser` is a Vue Single File Component (SFC) template parser for markuplint. It uses vue-eslint-parser to parse the `<template>` block of Vue SFCs into a vue-eslint-parser AST, then converts that AST into the unified markuplint AST format (`MLASTDocument`). The package handles Vue-specific directives (`v-bind`, `v-on`, `v-model`, `v-slot`), template expression containers (`{{ }}`), template comments, and PascalCase component detection.
6
+
7
+ ## Directory Structure
8
+
9
+ ```
10
+ src/
11
+ ├── index.ts — Re-exports parser
12
+ ├── parser.ts — VueParser class extending Parser<ASTNode, State>
13
+ ├── index.spec.ts — Integration tests for VueParser
14
+ └── vue-parser/
15
+ └── index.ts — vue-eslint-parser wrapper, ASTNode/ASTComment type exports
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
+ vueEslintParser["vue-eslint-parser\n(Vue SFC tokenizer)"]
26
+ end
27
+
28
+ subgraph pkg ["@markuplint/vue-parser"]
29
+ vueParser["VueParser\nextends Parser‹ASTNode, State›"]
30
+ vueParseFn["vueParse()\nvue-eslint-parser wrapper"]
31
+ end
32
+
33
+ subgraph downstream ["Downstream"]
34
+ mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
35
+ end
36
+
37
+ mlAst -->|"AST types"| vueParser
38
+ parserUtils -->|"Parser base class"| vueParser
39
+ vueEslintParser -->|"parse()"| vueParseFn
40
+ vueParseFn -->|"ESLintProgram AST"| vueParser
41
+
42
+ vueParser -->|"produces MLASTDocument"| mlCore
43
+ ```
44
+
45
+ ## VueParser Class
46
+
47
+ ### Inheritance
48
+
49
+ ```
50
+ Parser<ASTNode, State> (from @markuplint/parser-utils)
51
+ └── VueParser (this package)
52
+ ```
53
+
54
+ ### Constructor
55
+
56
+ The constructor configures the parser with two arguments:
57
+
58
+ | Argument | Value | Purpose |
59
+ | --------------- | ----------------------- | ------------------------------------------------------------------------ |
60
+ | `ParserOptions` | `{ endTagType: 'xml' }` | Vue templates use explicit closing tags (XML-style), not HTML void rules |
61
+ | Initial State | `{ comments: [] }` | Empty comments array, populated during `tokenize()` |
62
+
63
+ The `tagNameCaseSensitive` behavior is inherited from the base class and combined with Vue's `detectElementType` override to correctly handle PascalCase component names.
64
+
65
+ ### State Type
66
+
67
+ The parser maintains internal state through the `State` type:
68
+
69
+ | Field | Type | Purpose |
70
+ | ---------- | ----------------------- | -------------------------------------------------------------------------------------------------- |
71
+ | `comments` | `readonly ASTComment[]` | Template comments extracted from vue-eslint-parser during tokenize, injected later in flattenNodes |
72
+
73
+ ### Override Methods
74
+
75
+ | Method | Purpose |
76
+ | --------------------- | ---------------------------------------------------------------------------------------------- |
77
+ | `tokenize()` | Invokes vue-eslint-parser and extracts `templateBody.children` and comments |
78
+ | `parseError()` | Converts vue-eslint-parser `SyntaxError` (with `lineNumber`/`column`) into `ParserError` |
79
+ | `nodeize()` | Converts vue-eslint-parser AST nodes (VText, VElement, VExpressionContainer) to markuplint AST |
80
+ | `flattenNodes()` | Extends base flattening to inject template comments between sibling nodes |
81
+ | `afterFlattenNodes()` | Calls base with `exposeWhiteSpace: false`, `exposeInvalidNode: false`, `concatText: false` |
82
+ | `detectElementType()` | Detects PascalCase components and Vue built-in components |
83
+
84
+ > **Note:** The `visitAttr()` override has been removed. Vue directive handling (`v-bind`, `v-on`, `v-model`, `v-slot`, etc.) is now managed via `directivePatterns` in `@markuplint/vue-spec`.
85
+
86
+ ### `duplicatableAttrs`
87
+
88
+ A `Set<string>` containing `'class'` and `'style'` -- attributes that may appear multiple times on a single element (via `v-bind:class` alongside `class`).
89
+
90
+ ## tokenize()
91
+
92
+ The `tokenize()` method is the entry point for obtaining the vue-eslint-parser AST:
93
+
94
+ 1. Calls `vueParse(this.rawCode)` which invokes `VueESLintParser.parse(vueTemplate, { parser: false })`
95
+ 2. If `ast.templateBody?.comments` exists, stores them in `this.state.comments` for later injection
96
+ 3. Returns `{ ast: ast.templateBody?.children ?? [], isFragment: true }`
97
+
98
+ The `parser: false` option tells vue-eslint-parser to skip `<script>` parsing (only the `<template>` block is relevant for markuplint). If the source has no `<template>` block or it is empty, `templateBody?.children` returns `undefined` and the parser receives an empty array.
99
+
100
+ ## nodeize() Details
101
+
102
+ The `nodeize()` method dispatches based on the `originNode.type` field:
103
+
104
+ ### VText -> visitText
105
+
106
+ Text nodes are sliced from the source using `this.sliceFragment(range[0], range[1])` and passed to the base `visitText()` method with depth and parentNode.
107
+
108
+ ### VExpressionContainer -> visitPsBlock
109
+
110
+ Expression containers like `{{ expression }}` are converted to pseudo-block nodes via `visitPsBlock()`:
111
+
112
+ - `nodeName`: `'vue-expression-container'`
113
+ - `isFragment`: `false`
114
+
115
+ This treats Vue template expressions as opaque blocks in the markuplint AST rather than attempting to parse their JavaScript content.
116
+
117
+ ### VElement -> visitElement
118
+
119
+ For element nodes, the method:
120
+
121
+ 1. Slices the **start tag** token from `originNode.startTag.range`
122
+ 2. Calls `visitElement()` with the element's `name` and `namespace`
123
+ 3. Passes `originNode.children` as child nodes -- these are `templateBody.children` when the node represents the template root
124
+ 4. Creates an end tag token factory (`createEndTagToken`) that returns `null` if the element is self-closing, otherwise slices from `originNode.endTag.range`
125
+
126
+ ## flattenNodes()
127
+
128
+ The `flattenNodes()` method extends the base `Parser.flattenNodes()` to inject template comments:
129
+
130
+ 1. Calls `super.flattenNodes(nodeTree)` to get the initial flat node list
131
+ 2. Iterates through the node list, checking for comments between each pair of adjacent nodes
132
+ 3. For each gap between `prevNode.endOffset` (or `parentNode.endOffset` for the first node) and `node.startOffset`, searches `this.state.comments` for a comment whose range falls within that gap
133
+ 4. When a comment is found, creates it via `this.visitComment()` with `isBogus` set based on the comment's type (`HTMLBogusComment` vs standard)
134
+ 5. Appends the comment to the parent node via `this.appendChild()`
135
+
136
+ This two-pass approach is necessary because vue-eslint-parser provides comments separately from the main node tree, and they must be interleaved at the correct positions.
137
+
138
+ ## Directive Handling (directivePatterns in @markuplint/vue-spec)
139
+
140
+ Vue directive resolution is managed by `directivePatterns` defined in `@markuplint/vue-spec`, not by the parser itself. The spec declares patterns that the core engine uses to map directives to `potentialName`, `isDirective`, and `isDynamicValue` metadata.
141
+
142
+ > **Two-stage resolution:** Parser-level tests (`index.spec.ts`) show raw AST values where `isDynamicValue` and `isDirective` reflect only what the parser itself sets (e.g., curly-brace expressions). Core-level tests (`ml-core` and `rules`) show the final resolved values after `directivePatterns` are applied by `ml-core`'s `MLAttr` constructor. For example, `on:click` without a value shows `isDynamicValue: false` at the parser level, but resolves to `isDynamicValue: true` at the core level via the `directivePatterns` match.
143
+
144
+ ### Quote Set
145
+
146
+ The base parser handles standard HTML quotes (`"`, `'`). Vue templates also use `{}` as implicit value delimiters for expression bindings, though the attribute value itself uses standard quoting.
147
+
148
+ ### Vue Directive Processing
149
+
150
+ Directives are processed in priority order. The first matching pattern wins:
151
+
152
+ #### `v-on` / `@` (Event Binding)
153
+
154
+ - **Pattern**: `/^(v-on:|@)([^.]+)(?:\.([^.]+))?$/i`
155
+ - **Result**: `potentialName: 'on' + eventName.toLowerCase()`, `isDynamicValue: true`
156
+ - **Examples**:
157
+ - `@click` -> `potentialName: 'onclick'`
158
+ - `v-on:click.stop` -> `potentialName: 'onclick'`
159
+ - `@keydown.enter` -> `potentialName: 'onkeydown'`
160
+
161
+ #### `v-bind` / `:` (Property Binding)
162
+
163
+ - **Pattern**: `/^(v-bind:|:)([^.]+)(?:\.([^.]+))?$/i`
164
+ - **Result** (no modifier): `potentialName: propName`, `isDynamicValue: true`
165
+ - **Result** (`.attr` modifier): `potentialName: propName`, `isDynamicValue: true`
166
+ - **Result** (`.prop` / `.camel` / other modifiers): `isDirective: true`, `potentialName` set to normalized form
167
+ - **`isDuplicatable`**: If the bound property is in `duplicatableAttrs` (class, style), `isDuplicatable` is set to `true`
168
+ - **Examples**:
169
+ - `:data-attr` -> `potentialName: 'data-attr'`
170
+ - `v-bind:class` -> `potentialName: 'class'`, `isDuplicatable: true`
171
+ - `:title.attr` -> `potentialName: 'title'`
172
+ - `:foo.prop` -> `isDirective: true`
173
+
174
+ #### `v-model`
175
+
176
+ - **Pattern**: `/^(v-model)(?:\.([^.]+))?$/i`
177
+ - **Result**: `isDirective: true`
178
+ - **Examples**:
179
+ - `v-model` -> `isDirective: true`
180
+ - `v-model.lazy` -> `isDirective: true`
181
+
182
+ #### `v-slot` / `#` (Slot)
183
+
184
+ - **Pattern**: `/^(v-slot:|#)(.+)$/i`
185
+ - **Result**: `isDirective: true`, `potentialName: 'v-slot:' + slotName` (if different from raw name)
186
+ - **Examples**:
187
+ - `#header` -> `potentialName: 'v-slot:header'`, `isDirective: true`
188
+ - `v-slot:default` -> `isDirective: true`
189
+
190
+ #### Other `v-` Directives
191
+
192
+ - **Pattern**: Starts with `v-`
193
+ - **Result**: `isDirective: true`
194
+ - **Examples**: `v-if`, `v-for`, `v-show`, `v-else`, `v-else-if`, `v-pre`, `v-cloak`, `v-once`, `v-memo`, `v-html`, `v-text`
195
+
196
+ ## Element Type Detection
197
+
198
+ The `detectElementType()` method calls `super.detectElementType(nodeName, matchers)` with an array of matchers for Vue-specific component detection:
199
+
200
+ | Matcher | Type | Matches |
201
+ | ------------------- | ------ | ------------------------------------------- |
202
+ | `'Transition'` | String | Vue built-in `<Transition>` component |
203
+ | `'TransitionGroup'` | String | Vue built-in `<TransitionGroup>` component |
204
+ | `'KeepAlive'` | String | Vue built-in `<KeepAlive>` component |
205
+ | `'Teleport'` | String | Vue built-in `<Teleport>` component |
206
+ | `'Suspense'` | String | Vue built-in `<Suspense>` component |
207
+ | `'component'` | String | Vue special element `<component :is="...">` |
208
+ | `'slot'` | String | Vue special element `<slot>` |
209
+ | `/^[A-Z]/` | RegExp | Any PascalCase tag name (user components) |
210
+
211
+ When a tag name matches any of these, `detectElementType()` returns `'authored'` (indicating a component). Otherwise, standard HTML element detection applies:
212
+
213
+ - `div`, `span`, `p` etc. -> `'html'`
214
+ - `x-foo`, `my-element` -> `'web-component'`
215
+
216
+ Note that `<transition>` (lowercase) does **not** match the built-in list and is treated as a standard HTML element (`'html'`), while `<Transition>` (PascalCase) is treated as `'authored'`.
217
+
218
+ ## afterFlattenNodes()
219
+
220
+ The `afterFlattenNodes()` method calls the base implementation with specific options:
221
+
222
+ | Option | Value | Effect |
223
+ | ------------------- | ------- | -------------------------------------------------------------------- |
224
+ | `exposeWhiteSpace` | `false` | Whitespace-only text nodes are not exposed as separate invalid nodes |
225
+ | `exposeInvalidNode` | `false` | Invalid nodes are not exposed |
226
+ | `concatText` | `false` | Adjacent text nodes are not concatenated |
227
+
228
+ These settings reflect that Vue's template parser handles whitespace and node validity differently from raw HTML parsing.
229
+
230
+ ## Version Compatibility
231
+
232
+ The vue-eslint-parser dependency supports both Vue 2 and Vue 3 template syntax. The parser does not distinguish between Vue versions at the AST level -- both produce the same `VElement`, `VText`, and `VExpressionContainer` node types. Vue 3-specific features like `<Teleport>` and `<Suspense>` are handled through element type detection rather than parser-level changes.
233
+
234
+ ## Limitations
235
+
236
+ ### No `blockBehavior` support for `v-if` / `v-for`
237
+
238
+ Other framework parsers (Svelte, Pug, Alpine, JSX, Astro) set `blockBehavior` on their conditional/loop constructs so that the core engine can enumerate all possible child node patterns via `conditionalChildNodes()`. The Vue parser does **not** support this. As a result, rules like `permitted-contents` cannot validate content models across `v-if`/`v-else` branches or `v-for` iterations in Vue templates.
239
+
240
+ **Why this is difficult to implement:**
241
+
242
+ In Alpine.js, conditionals and loops use a fixed pattern — `<template x-for="...">` / `<template x-if="...">` — where the `<template>` element can be cleanly converted into a PSBlock. Vue's directives work fundamentally differently:
243
+
244
+ 1. **Directives attach to arbitrary elements**: `v-if`, `v-for`, `v-else`, and `v-else-if` can appear on any element (e.g., `<div v-if="...">`, `<li v-for="...">`). The element must remain a valid HTML element for attribute validation while simultaneously acting as a block for content model analysis — a dual role the current parser architecture does not support.
245
+
246
+ 2. **Sibling-based branching**: `v-else` and `v-else-if` are attributes on **sibling** elements, not child constructs of a wrapper block. Building conditional groups requires cross-sibling analysis that goes beyond the current per-node `nodeize()` model.
247
+
248
+ The current Vue parser handles these directives at the attribute level only (`isDirective: true`), which suppresses attribute validation errors but does not provide structural block information to the core engine.
249
+
250
+ ## Key Source Files
251
+
252
+ | File | Purpose |
253
+ | ------------------------- | -------------------------------------------------------------------- |
254
+ | `src/parser.ts` | VueParser class with all override methods |
255
+ | `src/vue-parser/index.ts` | vue-eslint-parser wrapper and type definitions (ASTNode, ASTComment) |
256
+ | `src/index.ts` | Module entry point, re-exports parser instance |
257
+ | `src/index.spec.ts` | Integration tests covering parsing, directives, namespaces |
258
+
259
+ ## External Dependencies
260
+
261
+ | Dependency | Purpose |
262
+ | -------------------------- | -------------------------------------------------------------------- |
263
+ | `@markuplint/ml-ast` | AST type definitions (`MLASTParentNode`, `MLASTNodeTreeItem`, etc.) |
264
+ | `@markuplint/parser-utils` | Abstract `Parser` class, `ParserError`, `Token`, `ChildToken` |
265
+ | `@markuplint/html-parser` | Peer dependency (not directly imported but part of parser ecosystem) |
266
+ | `vue-eslint-parser` | Vue SFC template parsing (`parse`, AST types) |
267
+
268
+ ## Integration Points
269
+
270
+ ```mermaid
271
+ flowchart TD
272
+ subgraph upstream ["Upstream"]
273
+ mlAst["@markuplint/ml-ast\n(AST types)"]
274
+ parserUtils["@markuplint/parser-utils\n(Parser base class)"]
275
+ vueEslintParser["vue-eslint-parser\n(Vue SFC tokenizer)"]
276
+ end
277
+
278
+ subgraph pkg ["@markuplint/vue-parser"]
279
+ vueParser["VueParser"]
280
+ end
281
+
282
+ subgraph downstream ["Downstream"]
283
+ mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
284
+ end
285
+
286
+ upstream -->|"types, parsing"| vueParser
287
+ vueParser -->|"produces MLASTDocument"| mlCore
288
+ ```
289
+
290
+ ### Upstream
291
+
292
+ - **`@markuplint/ml-ast`** -- AST type definitions used throughout the parser
293
+ - **`@markuplint/parser-utils`** -- Abstract `Parser` class that `VueParser` extends, plus `ParserError` and utility types
294
+ - **`vue-eslint-parser`** -- The underlying Vue SFC parser that performs template tokenization and tree construction
295
+
296
+ ### Downstream
297
+
298
+ - **`@markuplint/ml-core`** -- Consumes the `MLASTDocument` produced by `VueParser` and constructs the MLDOM for rule evaluation
299
+
300
+ ## Documentation Map
301
+
302
+ - [Maintenance Guide](docs/maintenance.md) -- Commands, recipes, and troubleshooting
package/CHANGELOG.md CHANGED
@@ -3,13 +3,29 @@
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
- ## [4.6.22](https://github.com/markuplint/markuplint/compare/@markuplint/vue-parser@4.6.21...@markuplint/vue-parser@4.6.22) (2025-11-05)
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
- **Note:** Version bump only for package @markuplint/vue-parser
8
+ ### Bug Fixes
9
+
10
+ - **ml-core:** improve detection of namespace ([5b507ad](https://github.com/markuplint/markuplint/commit/5b507ad7c19c5015b8ce587845d901e31dfa6518))
11
+ - **vue-parser:** update vue-eslint-parser to 10.3.0 and fix TS4053 errors ([b4633ea](https://github.com/markuplint/markuplint/commit/b4633eaeeb55c3b969127071094fde8e51bfb451))
12
+
13
+ - refactor(vue-parser)!: update for simplified AST token properties ([b7e52df](https://github.com/markuplint/markuplint/commit/b7e52df21b6af0a4f2b61b327e60ed609f4359cc))
14
+
15
+ ### BREAKING CHANGES
9
16
 
17
+ - Replace startOffset/endOffset with offset and
18
+ offset + raw.length in flattenNodes comment handling.
10
19
 
20
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11
21
 
22
+ ## [4.6.23](https://github.com/markuplint/markuplint/compare/@markuplint/vue-parser@4.6.22...@markuplint/vue-parser@4.6.23) (2026-02-10)
12
23
 
24
+ **Note:** Version bump only for package @markuplint/vue-parser
25
+
26
+ ## [4.6.22](https://github.com/markuplint/markuplint/compare/@markuplint/vue-parser@4.6.21...@markuplint/vue-parser@4.6.22) (2025-11-05)
27
+
28
+ **Note:** Version bump only for package @markuplint/vue-parser
13
29
 
14
30
  ## [4.6.21](https://github.com/markuplint/markuplint/compare/@markuplint/vue-parser@4.6.20...@markuplint/vue-parser@4.6.21) (2025-08-24)
15
31