@markuplint/vue-parser 4.6.21 → 4.6.23

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,283 @@
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
+ | `visitAttr()` | Vue ディレクティブの省略形を `potentialName`/`isDirective` メタデータに解決 |
83
+ | `detectElementType()` | PascalCase コンポーネントと Vue 組み込みコンポーネントを検出 |
84
+
85
+ ### `duplicatableAttrs`
86
+
87
+ `'class'` と `'style'` を含む `Set<string>` -- `v-bind:class` と `class` が同一要素に共存できるように、重複可能な属性を定義します。
88
+
89
+ ## tokenize()
90
+
91
+ `tokenize()` メソッドは vue-eslint-parser AST を取得するエントリーポイントです:
92
+
93
+ 1. `vueParse(this.rawCode)` を呼び出し、内部で `VueESLintParser.parse(vueTemplate, { parser: false })` を実行
94
+ 2. `ast.templateBody?.comments` が存在する場合、`this.state.comments` に格納して後の注入に備える
95
+ 3. `{ ast: ast.templateBody?.children ?? [], isFragment: true }` を返す
96
+
97
+ `parser: false` オプションは vue-eslint-parser に `<script>` のパースをスキップするよう指示します(markuplint にとって関連するのは `<template>` ブロックのみ)。ソースに `<template>` ブロックがないか空の場合、`templateBody?.children` は `undefined` を返し、パーサーは空の配列を受け取ります。
98
+
99
+ ## nodeize() の詳細
100
+
101
+ `nodeize()` メソッドは `originNode.type` フィールドに基づいてディスパッチします:
102
+
103
+ ### VText -> visitText
104
+
105
+ テキストノードは `this.sliceFragment(range[0], range[1])` でソースからスライスされ、depth と parentNode と共に基底の `visitText()` メソッドに渡されます。
106
+
107
+ ### VExpressionContainer -> visitPsBlock
108
+
109
+ `{{ expression }}` のような式コンテナは `visitPsBlock()` で擬似ブロックノードに変換されます:
110
+
111
+ - `nodeName`: `'vue-expression-container'`
112
+ - `isFragment`: `false`
113
+
114
+ これにより、Vue テンプレート式は JavaScript コンテンツのパースを試みるのではなく、markuplint AST 内で不透明なブロックとして扱われます。
115
+
116
+ ### VElement -> visitElement
117
+
118
+ 要素ノードの場合、メソッドは:
119
+
120
+ 1. `originNode.startTag.range` から**開始タグ**トークンをスライス
121
+ 2. 要素の `name` と `namespace` と共に `visitElement()` を呼び出す
122
+ 3. `originNode.children` を子ノードとして渡す -- テンプレートルートを表すノードの場合、これは `templateBody.children`
123
+ 4. エンドタグトークンファクトリ(`createEndTagToken`)を作成 -- 要素が自己閉じの場合は `null` を返し、そうでなければ `originNode.endTag.range` からスライス
124
+
125
+ ## flattenNodes()
126
+
127
+ `flattenNodes()` メソッドは基底の `Parser.flattenNodes()` を拡張してテンプレートコメントを注入します:
128
+
129
+ 1. `super.flattenNodes(nodeTree)` を呼び出して初期フラットノードリストを取得
130
+ 2. ノードリストを走査し、隣接するノードペア間のコメントを確認
131
+ 3. `prevNode.endOffset`(最初のノードの場合は `parentNode.endOffset`)と `node.startOffset` の間の各ギャップについて、そのギャップ内に範囲が収まるコメントを `this.state.comments` から検索
132
+ 4. コメントが見つかった場合、`this.visitComment()` で作成し、コメントの type に基づいて `isBogus` を設定(`HTMLBogusComment` の場合は true)
133
+ 5. `this.appendChild()` でコメントを親ノードに追加
134
+
135
+ この2パスアプローチが必要な理由は、vue-eslint-parser がコメントをメインノードツリーとは別に提供するため、正しい位置にインターリーブする必要があるからです。
136
+
137
+ ## 属性処理(visitAttr)
138
+
139
+ `visitAttr()` メソッドはまず `super.visitAttr(token)` を呼び出し、その後 Vue 固有のディレクティブ解決を適用します。
140
+
141
+ ### クォートセット
142
+
143
+ 基底パーサーは標準 HTML クォート(`"`、`'`)を処理します。Vue テンプレートは式バインディングの暗黙的な値デリミタとして `{}` も使用しますが、属性値自体は標準のクォーティングを使用します。
144
+
145
+ ### Vue ディレクティブ処理
146
+
147
+ ディレクティブは優先順位に従って処理されます。最初にマッチするパターンが適用されます:
148
+
149
+ #### `v-on` / `@`(イベントバインディング)
150
+
151
+ - **パターン**: `/^(v-on:|@)([^.]+)(?:\.([^.]+))?$/i`
152
+ - **結果**: `potentialName: 'on' + eventName.toLowerCase()`、`isDynamicValue: true`
153
+ - **例**:
154
+ - `@click` -> `potentialName: 'onclick'`
155
+ - `v-on:click.stop` -> `potentialName: 'onclick'`
156
+ - `@keydown.enter` -> `potentialName: 'onkeydown'`
157
+
158
+ #### `v-bind` / `:`(プロパティバインディング)
159
+
160
+ - **パターン**: `/^(v-bind:|:)([^.]+)(?:\.([^.]+))?$/i`
161
+ - **結果**(修飾子なし): `potentialName: propName`、`isDynamicValue: true`
162
+ - **結果**(`.attr` 修飾子): `potentialName: propName`、`isDynamicValue: true`
163
+ - **結果**(`.prop` / `.camel` / その他修飾子): `isDirective: true`、`potentialName` が正規化された形式に設定
164
+ - **`isDuplicatable`**: バインドされたプロパティが `duplicatableAttrs` に含まれる場合(class、style)、`isDuplicatable` が `true` に設定
165
+ - **例**:
166
+ - `:data-attr` -> `potentialName: 'data-attr'`
167
+ - `v-bind:class` -> `potentialName: 'class'`、`isDuplicatable: true`
168
+ - `:title.attr` -> `potentialName: 'title'`
169
+ - `:foo.prop` -> `isDirective: true`
170
+
171
+ #### `v-model`
172
+
173
+ - **パターン**: `/^(v-model)(?:\.([^.]+))?$/i`
174
+ - **結果**: `isDirective: true`
175
+ - **例**:
176
+ - `v-model` -> `isDirective: true`
177
+ - `v-model.lazy` -> `isDirective: true`
178
+
179
+ #### `v-slot` / `#`(スロット)
180
+
181
+ - **パターン**: `/^(v-slot:|#)(.+)$/i`
182
+ - **結果**: `isDirective: true`、`potentialName: 'v-slot:' + slotName`(raw name と異なる場合)
183
+ - **例**:
184
+ - `#header` -> `potentialName: 'v-slot:header'`、`isDirective: true`
185
+ - `v-slot:default` -> `isDirective: true`
186
+
187
+ #### その他の `v-` ディレクティブ
188
+
189
+ - **パターン**: `v-` で始まる
190
+ - **結果**: `isDirective: true`
191
+ - **例**: `v-if`、`v-for`、`v-show`、`v-else`、`v-else-if`、`v-pre`、`v-cloak`、`v-once`、`v-memo`、`v-html`、`v-text`
192
+
193
+ ## 要素タイプ検出
194
+
195
+ `detectElementType()` メソッドは Vue 固有のコンポーネント検出のためのマッチャー配列を使って `super.detectElementType(nodeName, matchers)` を呼び出します:
196
+
197
+ | マッチャー | 型 | マッチ対象 |
198
+ | ------------------- | ------ | ----------------------------------------------- |
199
+ | `'Transition'` | String | Vue 組み込み `<Transition>` コンポーネント |
200
+ | `'TransitionGroup'` | String | Vue 組み込み `<TransitionGroup>` コンポーネント |
201
+ | `'KeepAlive'` | String | Vue 組み込み `<KeepAlive>` コンポーネント |
202
+ | `'Teleport'` | String | Vue 組み込み `<Teleport>` コンポーネント |
203
+ | `'Suspense'` | String | Vue 組み込み `<Suspense>` コンポーネント |
204
+ | `'component'` | String | Vue 特殊要素 `<component :is="...">` |
205
+ | `'slot'` | String | Vue 特殊要素 `<slot>` |
206
+ | `/^[A-Z]/` | RegExp | PascalCase のタグ名(ユーザーコンポーネント) |
207
+
208
+ タグ名がこれらのいずれかにマッチする場合、`detectElementType()` は `'authored'`(コンポーネントを示す)を返します。それ以外は標準の HTML 要素検出が適用されます:
209
+
210
+ - `div`、`span`、`p` 等 -> `'html'`
211
+ - `x-foo`、`my-element` -> `'web-component'`
212
+
213
+ `<transition>`(小文字)は組み込みリストにマッチ**しない**ため標準 HTML 要素(`'html'`)として扱われますが、`<Transition>`(PascalCase)は `'authored'` として扱われることに注意してください。
214
+
215
+ ## afterFlattenNodes()
216
+
217
+ `afterFlattenNodes()` メソッドは特定のオプションで基底実装を呼び出します:
218
+
219
+ | オプション | 値 | 効果 |
220
+ | ------------------- | ------- | -------------------------------------------------------- |
221
+ | `exposeWhiteSpace` | `false` | 空白のみのテキストノードは別の無効ノードとして公開しない |
222
+ | `exposeInvalidNode` | `false` | 無効なノードは公開しない |
223
+ | `concatText` | `false` | 隣接するテキストノードは結合しない |
224
+
225
+ これらの設定は、Vue のテンプレートパーサーが空白やノードの妥当性を生の HTML パースとは異なる方法で処理することを反映しています。
226
+
227
+ ## バージョン互換性
228
+
229
+ vue-eslint-parser 依存は Vue 2 と Vue 3 の両方のテンプレート構文をサポートしています。パーサーは AST レベルで Vue バージョンを区別しません -- どちらも同じ `VElement`、`VText`、`VExpressionContainer` ノードタイプを生成します。Vue 3 固有の機能(`<Teleport>` や `<Suspense>` など)はパーサーレベルの変更ではなく、要素タイプ検出を通じて処理されます。
230
+
231
+ ## 主要ソースファイル
232
+
233
+ | ファイル | 用途 |
234
+ | ------------------------- | ----------------------------------------------------------------- |
235
+ | `src/parser.ts` | 全オーバーライドメソッドを持つ VueParser クラス |
236
+ | `src/vue-parser/index.ts` | vue-eslint-parser ラッパーと型定義(ASTNode、ASTComment) |
237
+ | `src/index.ts` | モジュールエントリーポイント、parser インスタンスを再エクスポート |
238
+ | `src/index.spec.ts` | パース、ディレクティブ、名前空間をカバーする統合テスト |
239
+
240
+ ## 外部依存
241
+
242
+ | 依存パッケージ | 用途 |
243
+ | -------------------------- | ---------------------------------------------------------------- |
244
+ | `@markuplint/ml-ast` | AST 型定義(`MLASTParentNode`、`MLASTNodeTreeItem` 等) |
245
+ | `@markuplint/parser-utils` | 抽象 `Parser` クラス、`ParserError`、`Token`、`ChildToken` |
246
+ | `@markuplint/html-parser` | ピア依存(直接インポートされないが、パーサーエコシステムの一部) |
247
+ | `vue-eslint-parser` | Vue SFC テンプレートパース(`parse`、AST 型) |
248
+
249
+ ## 統合ポイント
250
+
251
+ ```mermaid
252
+ flowchart TD
253
+ subgraph upstream ["上流"]
254
+ mlAst["@markuplint/ml-ast\n(AST 型定義)"]
255
+ parserUtils["@markuplint/parser-utils\n(Parser 基底クラス)"]
256
+ vueEslintParser["vue-eslint-parser\n(Vue SFC トークナイザ)"]
257
+ end
258
+
259
+ subgraph pkg ["@markuplint/vue-parser"]
260
+ vueParser["VueParser"]
261
+ end
262
+
263
+ subgraph downstream ["下流"]
264
+ mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
265
+ end
266
+
267
+ upstream -->|"型、パース"| vueParser
268
+ vueParser -->|"MLASTDocument を生成"| mlCore
269
+ ```
270
+
271
+ ### 上流
272
+
273
+ - **`@markuplint/ml-ast`** -- パーサー全体で使用される AST 型定義
274
+ - **`@markuplint/parser-utils`** -- `VueParser` が拡張する抽象 `Parser` クラスと `ParserError` およびユーティリティ型
275
+ - **`vue-eslint-parser`** -- テンプレートのトークン化とツリー構築を行う基盤 Vue SFC パーサー
276
+
277
+ ### 下流
278
+
279
+ - **`@markuplint/ml-core`** -- `VueParser` が生成する `MLASTDocument` を消費し、ルール評価のための MLDOM を構築
280
+
281
+ ## ドキュメントマップ
282
+
283
+ - [メンテナンスガイド](docs/maintenance.ja.md) -- コマンド、レシピ、トラブルシューティング
@@ -0,0 +1,283 @@
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
+ | `visitAttr()` | Resolves Vue directive shorthands to `potentialName`/`isDirective` metadata |
83
+ | `detectElementType()` | Detects PascalCase components and Vue built-in components |
84
+
85
+ ### `duplicatableAttrs`
86
+
87
+ A `Set<string>` containing `'class'` and `'style'` -- attributes that may appear multiple times on a single element (via `v-bind:class` alongside `class`).
88
+
89
+ ## tokenize()
90
+
91
+ The `tokenize()` method is the entry point for obtaining the vue-eslint-parser AST:
92
+
93
+ 1. Calls `vueParse(this.rawCode)` which invokes `VueESLintParser.parse(vueTemplate, { parser: false })`
94
+ 2. If `ast.templateBody?.comments` exists, stores them in `this.state.comments` for later injection
95
+ 3. Returns `{ ast: ast.templateBody?.children ?? [], isFragment: true }`
96
+
97
+ 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.
98
+
99
+ ## nodeize() Details
100
+
101
+ The `nodeize()` method dispatches based on the `originNode.type` field:
102
+
103
+ ### VText -> visitText
104
+
105
+ 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.
106
+
107
+ ### VExpressionContainer -> visitPsBlock
108
+
109
+ Expression containers like `{{ expression }}` are converted to pseudo-block nodes via `visitPsBlock()`:
110
+
111
+ - `nodeName`: `'vue-expression-container'`
112
+ - `isFragment`: `false`
113
+
114
+ This treats Vue template expressions as opaque blocks in the markuplint AST rather than attempting to parse their JavaScript content.
115
+
116
+ ### VElement -> visitElement
117
+
118
+ For element nodes, the method:
119
+
120
+ 1. Slices the **start tag** token from `originNode.startTag.range`
121
+ 2. Calls `visitElement()` with the element's `name` and `namespace`
122
+ 3. Passes `originNode.children` as child nodes -- these are `templateBody.children` when the node represents the template root
123
+ 4. Creates an end tag token factory (`createEndTagToken`) that returns `null` if the element is self-closing, otherwise slices from `originNode.endTag.range`
124
+
125
+ ## flattenNodes()
126
+
127
+ The `flattenNodes()` method extends the base `Parser.flattenNodes()` to inject template comments:
128
+
129
+ 1. Calls `super.flattenNodes(nodeTree)` to get the initial flat node list
130
+ 2. Iterates through the node list, checking for comments between each pair of adjacent nodes
131
+ 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
132
+ 4. When a comment is found, creates it via `this.visitComment()` with `isBogus` set based on the comment's type (`HTMLBogusComment` vs standard)
133
+ 5. Appends the comment to the parent node via `this.appendChild()`
134
+
135
+ 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.
136
+
137
+ ## Attribute Processing (visitAttr)
138
+
139
+ The `visitAttr()` method calls `super.visitAttr(token)` first, then applies Vue-specific directive resolution.
140
+
141
+ ### Quote Set
142
+
143
+ 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.
144
+
145
+ ### Vue Directive Processing
146
+
147
+ Directives are processed in priority order. The first matching pattern wins:
148
+
149
+ #### `v-on` / `@` (Event Binding)
150
+
151
+ - **Pattern**: `/^(v-on:|@)([^.]+)(?:\.([^.]+))?$/i`
152
+ - **Result**: `potentialName: 'on' + eventName.toLowerCase()`, `isDynamicValue: true`
153
+ - **Examples**:
154
+ - `@click` -> `potentialName: 'onclick'`
155
+ - `v-on:click.stop` -> `potentialName: 'onclick'`
156
+ - `@keydown.enter` -> `potentialName: 'onkeydown'`
157
+
158
+ #### `v-bind` / `:` (Property Binding)
159
+
160
+ - **Pattern**: `/^(v-bind:|:)([^.]+)(?:\.([^.]+))?$/i`
161
+ - **Result** (no modifier): `potentialName: propName`, `isDynamicValue: true`
162
+ - **Result** (`.attr` modifier): `potentialName: propName`, `isDynamicValue: true`
163
+ - **Result** (`.prop` / `.camel` / other modifiers): `isDirective: true`, `potentialName` set to normalized form
164
+ - **`isDuplicatable`**: If the bound property is in `duplicatableAttrs` (class, style), `isDuplicatable` is set to `true`
165
+ - **Examples**:
166
+ - `:data-attr` -> `potentialName: 'data-attr'`
167
+ - `v-bind:class` -> `potentialName: 'class'`, `isDuplicatable: true`
168
+ - `:title.attr` -> `potentialName: 'title'`
169
+ - `:foo.prop` -> `isDirective: true`
170
+
171
+ #### `v-model`
172
+
173
+ - **Pattern**: `/^(v-model)(?:\.([^.]+))?$/i`
174
+ - **Result**: `isDirective: true`
175
+ - **Examples**:
176
+ - `v-model` -> `isDirective: true`
177
+ - `v-model.lazy` -> `isDirective: true`
178
+
179
+ #### `v-slot` / `#` (Slot)
180
+
181
+ - **Pattern**: `/^(v-slot:|#)(.+)$/i`
182
+ - **Result**: `isDirective: true`, `potentialName: 'v-slot:' + slotName` (if different from raw name)
183
+ - **Examples**:
184
+ - `#header` -> `potentialName: 'v-slot:header'`, `isDirective: true`
185
+ - `v-slot:default` -> `isDirective: true`
186
+
187
+ #### Other `v-` Directives
188
+
189
+ - **Pattern**: Starts with `v-`
190
+ - **Result**: `isDirective: true`
191
+ - **Examples**: `v-if`, `v-for`, `v-show`, `v-else`, `v-else-if`, `v-pre`, `v-cloak`, `v-once`, `v-memo`, `v-html`, `v-text`
192
+
193
+ ## Element Type Detection
194
+
195
+ The `detectElementType()` method calls `super.detectElementType(nodeName, matchers)` with an array of matchers for Vue-specific component detection:
196
+
197
+ | Matcher | Type | Matches |
198
+ | ------------------- | ------ | ------------------------------------------- |
199
+ | `'Transition'` | String | Vue built-in `<Transition>` component |
200
+ | `'TransitionGroup'` | String | Vue built-in `<TransitionGroup>` component |
201
+ | `'KeepAlive'` | String | Vue built-in `<KeepAlive>` component |
202
+ | `'Teleport'` | String | Vue built-in `<Teleport>` component |
203
+ | `'Suspense'` | String | Vue built-in `<Suspense>` component |
204
+ | `'component'` | String | Vue special element `<component :is="...">` |
205
+ | `'slot'` | String | Vue special element `<slot>` |
206
+ | `/^[A-Z]/` | RegExp | Any PascalCase tag name (user components) |
207
+
208
+ When a tag name matches any of these, `detectElementType()` returns `'authored'` (indicating a component). Otherwise, standard HTML element detection applies:
209
+
210
+ - `div`, `span`, `p` etc. -> `'html'`
211
+ - `x-foo`, `my-element` -> `'web-component'`
212
+
213
+ 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'`.
214
+
215
+ ## afterFlattenNodes()
216
+
217
+ The `afterFlattenNodes()` method calls the base implementation with specific options:
218
+
219
+ | Option | Value | Effect |
220
+ | ------------------- | ------- | -------------------------------------------------------------------- |
221
+ | `exposeWhiteSpace` | `false` | Whitespace-only text nodes are not exposed as separate invalid nodes |
222
+ | `exposeInvalidNode` | `false` | Invalid nodes are not exposed |
223
+ | `concatText` | `false` | Adjacent text nodes are not concatenated |
224
+
225
+ These settings reflect that Vue's template parser handles whitespace and node validity differently from raw HTML parsing.
226
+
227
+ ## Version Compatibility
228
+
229
+ 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.
230
+
231
+ ## Key Source Files
232
+
233
+ | File | Purpose |
234
+ | ------------------------- | -------------------------------------------------------------------- |
235
+ | `src/parser.ts` | VueParser class with all override methods |
236
+ | `src/vue-parser/index.ts` | vue-eslint-parser wrapper and type definitions (ASTNode, ASTComment) |
237
+ | `src/index.ts` | Module entry point, re-exports parser instance |
238
+ | `src/index.spec.ts` | Integration tests covering parsing, directives, namespaces |
239
+
240
+ ## External Dependencies
241
+
242
+ | Dependency | Purpose |
243
+ | -------------------------- | -------------------------------------------------------------------- |
244
+ | `@markuplint/ml-ast` | AST type definitions (`MLASTParentNode`, `MLASTNodeTreeItem`, etc.) |
245
+ | `@markuplint/parser-utils` | Abstract `Parser` class, `ParserError`, `Token`, `ChildToken` |
246
+ | `@markuplint/html-parser` | Peer dependency (not directly imported but part of parser ecosystem) |
247
+ | `vue-eslint-parser` | Vue SFC template parsing (`parse`, AST types) |
248
+
249
+ ## Integration Points
250
+
251
+ ```mermaid
252
+ flowchart TD
253
+ subgraph upstream ["Upstream"]
254
+ mlAst["@markuplint/ml-ast\n(AST types)"]
255
+ parserUtils["@markuplint/parser-utils\n(Parser base class)"]
256
+ vueEslintParser["vue-eslint-parser\n(Vue SFC tokenizer)"]
257
+ end
258
+
259
+ subgraph pkg ["@markuplint/vue-parser"]
260
+ vueParser["VueParser"]
261
+ end
262
+
263
+ subgraph downstream ["Downstream"]
264
+ mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
265
+ end
266
+
267
+ upstream -->|"types, parsing"| vueParser
268
+ vueParser -->|"produces MLASTDocument"| mlCore
269
+ ```
270
+
271
+ ### Upstream
272
+
273
+ - **`@markuplint/ml-ast`** -- AST type definitions used throughout the parser
274
+ - **`@markuplint/parser-utils`** -- Abstract `Parser` class that `VueParser` extends, plus `ParserError` and utility types
275
+ - **`vue-eslint-parser`** -- The underlying Vue SFC parser that performs template tokenization and tree construction
276
+
277
+ ### Downstream
278
+
279
+ - **`@markuplint/ml-core`** -- Consumes the `MLASTDocument` produced by `VueParser` and constructs the MLDOM for rule evaluation
280
+
281
+ ## Documentation Map
282
+
283
+ - [Maintenance Guide](docs/maintenance.md) -- Commands, recipes, and troubleshooting
package/CHANGELOG.md CHANGED
@@ -3,13 +3,17 @@
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.21](https://github.com/markuplint/markuplint/compare/@markuplint/vue-parser@4.6.20...@markuplint/vue-parser@4.6.21) (2025-08-24)
6
+ ## [4.6.23](https://github.com/markuplint/markuplint/compare/@markuplint/vue-parser@4.6.22...@markuplint/vue-parser@4.6.23) (2026-02-10)
7
7
 
8
8
  **Note:** Version bump only for package @markuplint/vue-parser
9
9
 
10
+ ## [4.6.22](https://github.com/markuplint/markuplint/compare/@markuplint/vue-parser@4.6.21...@markuplint/vue-parser@4.6.22) (2025-11-05)
10
11
 
12
+ **Note:** Version bump only for package @markuplint/vue-parser
11
13
 
14
+ ## [4.6.21](https://github.com/markuplint/markuplint/compare/@markuplint/vue-parser@4.6.20...@markuplint/vue-parser@4.6.21) (2025-08-24)
12
15
 
16
+ **Note:** Version bump only for package @markuplint/vue-parser
13
17
 
14
18
  ## [4.6.20](https://github.com/markuplint/markuplint/compare/@markuplint/vue-parser@4.6.19...@markuplint/vue-parser@4.6.20) (2025-08-13)
15
19
 
package/SKILL.md ADDED
@@ -0,0 +1,137 @@
1
+ ---
2
+ description: Maintenance tasks for @markuplint/vue-parser
3
+ globs:
4
+ - packages/@markuplint/vue-parser/src/**/*.ts
5
+ alwaysApply: false
6
+ ---
7
+
8
+ # vue-parser-maintenance
9
+
10
+ Perform maintenance tasks for `@markuplint/vue-parser`: add Vue directives, modify element type
11
+ detection, update vue-eslint-parser version support, and fix comment injection.
12
+
13
+ ## Input
14
+
15
+ `$ARGUMENTS` specifies the task. Supported tasks:
16
+
17
+ | Task | Description |
18
+ | ------------------------------- | ------------------------------------------------ |
19
+ | `add-directive` | Add or modify a Vue directive in visitAttr() |
20
+ | `modify-element-type-detection` | Update detectElementType() matchers |
21
+ | `update-vue-version-support` | Handle vue-eslint-parser API changes |
22
+ | `fix-comment-injection` | Fix template comment injection in flattenNodes() |
23
+
24
+ If omitted, defaults to `add-directive`.
25
+
26
+ ## Reference
27
+
28
+ Before executing any task, read `docs/maintenance.md` (or `docs/maintenance.ja.md`)
29
+ for the full guide. The recipes there are the source of truth for procedures.
30
+
31
+ Also read:
32
+
33
+ - `ARCHITECTURE.md` -- Package overview, directive processing, and element type detection
34
+ - `src/parser.ts` -- VueParser class (source of truth for all override methods)
35
+
36
+ ## Task: add-directive
37
+
38
+ Add or modify a Vue directive in `visitAttr()`. Follow recipe #1 in `docs/maintenance.md`.
39
+
40
+ ### Step 1: Understand the directive
41
+
42
+ 1. Read `src/parser.ts` and find the `visitAttr()` method
43
+ 2. Identify where in the priority chain the new directive should be processed
44
+ 3. Determine whether the directive needs `potentialName`, `isDirective`, or `isDynamicValue`
45
+
46
+ ### Step 2: Add the directive pattern
47
+
48
+ 1. Create a new regex block following the existing pattern (scoped block with destructuring)
49
+ 2. Use `attr.name.raw.match()` to extract the directive prefix and value
50
+ 3. Set the appropriate metadata:
51
+ - `potentialName` — maps the directive to an equivalent HTML attribute name
52
+ - `isDirective` — marks as a Vue-only directive with no HTML equivalent
53
+ - `isDynamicValue` — indicates the attribute value is a JavaScript expression
54
+ 4. Check if the attribute should be in `duplicatableAttrs` (e.g., class, style)
55
+
56
+ ### Step 3: Verify
57
+
58
+ 1. Build: `yarn build --scope @markuplint/vue-parser`
59
+ 2. Add test cases to `src/index.spec.ts` covering:
60
+ - The directive with its full form (e.g., `v-bind:prop`)
61
+ - The shorthand form if applicable (e.g., `:prop`)
62
+ - Edge cases with modifiers (e.g., `.stop`, `.lazy`)
63
+ 3. Test: `yarn test --scope @markuplint/vue-parser`
64
+
65
+ ## Task: modify-element-type-detection
66
+
67
+ Update `detectElementType()` matchers. Follow recipe #2 in `docs/maintenance.md`.
68
+
69
+ ### Step 1: Understand current matchers
70
+
71
+ 1. Read the `detectElementType()` method in `src/parser.ts`
72
+ 2. Review the matcher array: string literals for built-in components, regex for PascalCase
73
+
74
+ ### Step 2: Make the change
75
+
76
+ 1. To add a new built-in component: add the component name as a string to the matcher array
77
+ 2. To add a new pattern: add a regex to the matcher array
78
+ 3. Components matching any entry return `'authored'` element type
79
+
80
+ ### Step 3: Verify
81
+
82
+ 1. Build: `yarn build --scope @markuplint/vue-parser`
83
+ 2. Add test cases to the `elementType` test block in `src/index.spec.ts`
84
+ 3. Test: `yarn test --scope @markuplint/vue-parser`
85
+
86
+ ## Task: update-vue-version-support
87
+
88
+ Handle vue-eslint-parser API changes. Follow recipe #3 in `docs/maintenance.md`.
89
+
90
+ ### Step 1: Understand the change
91
+
92
+ 1. Read `src/vue-parser/index.ts` — the wrapper around vue-eslint-parser
93
+ 2. Check the vue-eslint-parser changelog for breaking changes
94
+ 3. Verify the `ASTNode` and `ASTComment` type exports still match
95
+
96
+ ### Step 2: Make the change
97
+
98
+ 1. Update `vueParse()` if the `parse()` API signature changed
99
+ 2. Update type exports (`ASTNode`, `ASTComment`, `VueTokens`) if AST types changed
100
+ 3. Update `tokenize()` in `src/parser.ts` if `templateBody` structure changed
101
+
102
+ ### Step 3: Verify
103
+
104
+ 1. Build: `yarn build --scope @markuplint/vue-parser`
105
+ 2. Test: `yarn test --scope @markuplint/vue-parser`
106
+ 3. Test with real Vue SFC files to ensure correct parsing
107
+
108
+ ## Task: fix-comment-injection
109
+
110
+ Fix template comment injection in `flattenNodes()`. Follow recipe #4 in `docs/maintenance.md`.
111
+
112
+ ### Step 1: Understand the issue
113
+
114
+ 1. Read the `flattenNodes()` method in `src/parser.ts`
115
+ 2. Understand the two-pass approach: first flatten, then inject comments
116
+ 3. Check how `this.state.comments` is populated in `tokenize()`
117
+
118
+ ### Step 2: Fix the injection logic
119
+
120
+ 1. Verify the comment range check: `lastOffset <= comment.range[0] && comment.range[1] <= node.startOffset`
121
+ 2. Verify `this.visitComment()` is called with the correct `isBogus` flag
122
+ 3. Verify `this.appendChild()` correctly attaches the comment to the parent
123
+
124
+ ### Step 3: Verify
125
+
126
+ 1. Build: `yarn build --scope @markuplint/vue-parser`
127
+ 2. Test with Vue templates containing HTML comments (`<!-- -->`), bogus comments (`<!...>`), and mixed content
128
+ 3. Test: `yarn test --scope @markuplint/vue-parser`
129
+
130
+ ## Rules
131
+
132
+ 1. **Use vue-eslint-parser** for all template parsing — never parse Vue templates manually.
133
+ 2. **Use `potentialName`** for directives that map to HTML attributes (e.g., `@click` -> `onclick`).
134
+ 3. **Use `isDirective: true`** for directives with no HTML equivalent (e.g., `v-if`, `v-for`).
135
+ 4. **Test with `nodeListToDebugMaps`** — this is the standard assertion pattern for parser tests.
136
+ 5. **Add JSDoc comments** to all new public methods and properties.
137
+ 6. **Preserve directive priority order** in `visitAttr()` — `v-on` before `v-bind` before `v-model` before `v-slot` before generic `v-`.
@@ -0,0 +1,203 @@
1
+ # メンテナンスガイド
2
+
3
+ ## コマンド
4
+
5
+ | コマンド | 説明 |
6
+ | ------------------------------------------- | ---------------------- |
7
+ | `yarn build --scope @markuplint/vue-parser` | このパッケージをビルド |
8
+ | `yarn dev --scope @markuplint/vue-parser` | ウォッチモードでビルド |
9
+ | `yarn clean --scope @markuplint/vue-parser` | ビルド成果物を削除 |
10
+ | `yarn test --scope @markuplint/vue-parser` | テストを実行 |
11
+
12
+ ## テスト
13
+
14
+ テストファイルは `*.spec.ts` の命名規則に従い、`src/` ディレクトリに配置されています:
15
+
16
+ | テストファイル | カバレッジ |
17
+ | --------------- | ------------------------------------------------------------------------------ |
18
+ | `index.spec.ts` | VueParser 統合テスト(パース、ディレクティブ、名前空間、要素タイプ、コメント) |
19
+
20
+ 主なテストパターンでは `nodeListToDebugMaps` を使用したスナップショット形式のアサーションを行います:
21
+
22
+ ```ts
23
+ import { nodeListToDebugMaps } from '@markuplint/parser-utils';
24
+ import { parser } from '@markuplint/vue-parser';
25
+
26
+ const doc = parser.parse('<template><div class="foo">text</div></template>');
27
+ const debugMaps = nodeListToDebugMaps(doc.nodeList, true);
28
+ expect(debugMaps).toStrictEqual([
29
+ // 期待されるデバッグ出力
30
+ ]);
31
+ ```
32
+
33
+ 属性メタデータのテスト(ディレクティブ、potentialName、isDynamicValue):
34
+
35
+ ```ts
36
+ const doc = parser.parse('<template><div v-bind:title="val"></div></template>');
37
+ expect(doc.nodeList[0].attributes[0].potentialName).toBe('title');
38
+ expect(doc.nodeList[0].attributes[0].isDynamicValue).toBeTruthy();
39
+ ```
40
+
41
+ 要素タイプのテスト:
42
+
43
+ ```ts
44
+ const doc = parser.parse('<template><MyComponent/></template>');
45
+ expect(doc.nodeList[0].elementType).toBe('authored');
46
+ ```
47
+
48
+ ## レシピ
49
+
50
+ ### 1. Vue ディレクティブの追加・変更
51
+
52
+ 1. `src/parser.ts` を読み、`visitAttr()` メソッドを見つける
53
+ 2. 優先チェーン内の正しい位置を特定:
54
+ - `v-on` / `@`(イベントバインディング) — 最初
55
+ - `v-bind` / `:`(プロパティバインディング) — 2番目
56
+ - `v-model` — 3番目
57
+ - `v-slot` / `#` — 4番目
58
+ - 汎用 `v-*` — 最後(キャッチオール)
59
+ 3. 正規表現パターンを持つ新しいスコープブロックを作成:
60
+ ```ts
61
+ {
62
+ const [, directive, name] = attr.name.raw.match(/^(v-newdir:|shorthand)(.+)$/i) ?? [];
63
+ if (directive && name) {
64
+ return {
65
+ ...attr,
66
+ potentialName: name, // HTML 属性にマッピングする場合
67
+ isDirective: true as const, // Vue 専用の場合
68
+ isDynamicValue: true as const, // 値が JavaScript の場合
69
+ };
70
+ }
71
+ }
72
+ ```
73
+ 4. ビルド: `yarn build --scope @markuplint/vue-parser`
74
+ 5. `src/index.spec.ts` にテストを追加:
75
+ - 完全形: `v-newdir:value`
76
+ - 省略形(該当する場合)
77
+ - 修飾子付き(該当する場合)
78
+ 6. テスト: `yarn test --scope @markuplint/vue-parser`
79
+
80
+ ### 2. 要素タイプ検出の変更
81
+
82
+ 1. `src/parser.ts` を読み、`detectElementType()` メソッドを見つける
83
+ 2. マッチャー配列は以下をサポート:
84
+ - **文字列リテラル** — 完全一致(例: `'Transition'`、`'component'`、`'slot'`)
85
+ - **RegExp** — パターンマッチ(例: PascalCase 用の `/^[A-Z]/`)
86
+ 3. 新しい Vue 組み込みコンポーネントを追加するには:
87
+ ```ts
88
+ detectElementType(nodeName: string) {
89
+ return super.detectElementType(nodeName, [
90
+ // Built-in components
91
+ 'Transition',
92
+ 'TransitionGroup',
93
+ 'KeepAlive',
94
+ 'Teleport',
95
+ 'Suspense',
96
+ 'NewBuiltIn', // <-- ここに追加
97
+ // Special elements
98
+ 'component',
99
+ 'slot',
100
+ // Backward compatibility
101
+ /^[A-Z]/,
102
+ ]);
103
+ }
104
+ ```
105
+ 4. ビルドとテスト: `yarn build --scope @markuplint/vue-parser && yarn test --scope @markuplint/vue-parser`
106
+ 5. `src/index.spec.ts` の `elementType` テストブロックにテストケースを追加
107
+
108
+ ### 3. vue-eslint-parser バージョンサポートの更新
109
+
110
+ 1. `src/vue-parser/index.ts` を読む — vue-eslint-parser のラッパー
111
+ 2. vue-eslint-parser のリリースノートで破壊的変更を確認
112
+ 3. 主な統合ポイント:
113
+ - `VueESLintParser.parse(vueTemplate, { parser: false })` — メインのパース呼び出し
114
+ - `ast.templateBody?.children` — テンプレートの子ノード
115
+ - `ast.templateBody?.comments` — テンプレートのコメント
116
+ - `VueESLintParser.AST.VElement` / `VText` / `VExpressionContainer` — ノードタイプ
117
+ 4. AST 型が変更された場合、型エクスポートを更新:
118
+ ```ts
119
+ export type ASTNode =
120
+ | VueESLintParser.AST.VElement
121
+ | VueESLintParser.AST.VText
122
+ | VueESLintParser.AST.VExpressionContainer;
123
+ ```
124
+ 5. ビルドとテスト: `yarn build --scope @markuplint/vue-parser && yarn test --scope @markuplint/vue-parser`
125
+
126
+ ### 4. テンプレートコメント注入の修正
127
+
128
+ 1. `src/parser.ts` の `flattenNodes()` メソッドを読む
129
+ 2. コメント注入のロジック:
130
+ - コメントは `tokenize()` 中に `this.state.comments` に格納される
131
+ - フラット化中、隣接するノードペアごとにコメントがその間にあるか確認
132
+ - 範囲チェック: `lastOffset <= comment.range[0] && comment.range[1] <= node.startOffset`
133
+ 3. よくある問題:
134
+ - コメントが注入されない: 範囲チェックがギャップを正しくカバーしているか確認
135
+ - コメントが間違った位置にある: `lastOffset` の計算を確認(`prevNode?.endOffset ?? node.parentNode?.endOffset ?? 0`)
136
+ - ボーガスコメント検出: `betweenComment.type === 'HTMLBogusComment'` を検証
137
+ 4. ビルドとテスト: `yarn build --scope @markuplint/vue-parser && yarn test --scope @markuplint/vue-parser`
138
+
139
+ ## 上流影響チェックリスト
140
+
141
+ 上流パッケージへの変更がこのパッケージに影響を与える可能性があります:
142
+
143
+ | パッケージ | 影響 |
144
+ | -------------------------- | ------------------------------------------------------------------------ |
145
+ | `@markuplint/parser-utils` | 基底 `Parser` クラスの変更が全オーバーライドメソッドに影響する可能性あり |
146
+ | `@markuplint/ml-ast` | AST 型の変更が nodeize() の戻り値の型の更新を必要とする可能性あり |
147
+ | `vue-eslint-parser` | AST 構造の変更が tokenize()、nodeize()、型の更新を必要とする可能性あり |
148
+
149
+ 上流パッケージが変更された場合、以下を実行:
150
+
151
+ ```shell
152
+ yarn test --scope @markuplint/vue-parser
153
+ ```
154
+
155
+ ## トラブルシューティング
156
+
157
+ ### Vue ディレクティブが認識されない
158
+
159
+ **症状:** `v-custom` のような Vue ディレクティブが `isDirective: true` としてマークされず、通常の HTML 属性として扱われる。
160
+
161
+ **原因:** `visitAttr()` でディレクティブパターンがマッチしないか、新しいディレクティブブロックが汎用 `v-*` キャッチオールの後に配置されている。
162
+
163
+ **解決策:**
164
+
165
+ 1. ディレクティブブロックの正規表現パターンを確認 — 完全な属性名にマッチすることを確認
166
+ 2. ブロックが `visitAttr()` 末尾の汎用 `v-*` キャッチオールの前に配置されていることを確認
167
+ 3. 返却オブジェクトに `isDirective: true as const` が含まれていることを検証
168
+
169
+ ### テンプレートコメントが AST に含まれていない
170
+
171
+ **症状:** Vue テンプレート内の HTML コメント(`<!-- ... -->`)がパースされたノードリストに存在しない。
172
+
173
+ **原因:** `flattenNodes()` でコメントが注入されていないか、`tokenize()` でキャプチャされていない。
174
+
175
+ **解決策:**
176
+
177
+ 1. `tokenize()` を確認 — `ast.templateBody?.comments` が `this.state.comments` に格納されていることを検証
178
+ 2. `flattenNodes()` を確認 — 範囲チェックロジックが隣接ノード間のコメントを見つけていることを検証
179
+ 3. 特定のコメント位置でテストケースを追加し、`nodeListToDebugMaps` 出力を確認
180
+
181
+ ### PascalCase コンポーネントが 'authored' として検出されない
182
+
183
+ **症状:** `<MyComponent>` のようなコンポーネントが `'authored'` ではなく `elementType: 'html'` となる。
184
+
185
+ **原因:** `detectElementType()` 内の `/^[A-Z]/` 正規表現がマッチしていないか、マッチャー配列が誤って構成されている。
186
+
187
+ **解決策:**
188
+
189
+ 1. コンポーネント名が大文字で始まることを確認
190
+ 2. `/^[A-Z]/` 正規表現がマッチャー配列に存在することを検証
191
+ 3. `<component>` や `<slot>` のような小文字の Vue 組み込みは正規表現ではなく文字列でマッチされることに注意
192
+
193
+ ### SyntaxError が正しく報告されない
194
+
195
+ **症状:** Vue テンプレートの構文エラーが `ParserError` を生成する代わりにプロセスをクラッシュさせる。
196
+
197
+ **原因:** `parseError()` メソッドがエラーをキャッチしていないか、エラーオブジェクトに `lineNumber`/`column` プロパティがない。
198
+
199
+ **解決策:**
200
+
201
+ 1. `parseError()` を確認 — `instanceof SyntaxError` と `'lineNumber' in error` のチェックを検証
202
+ 2. vue-eslint-parser のエラーには `lineNumber`(1ベース)と `column`(0ベース)が含まれる
203
+ 3. フォールバックの `super.parseError(error)` は非 SyntaxError ケースを処理する
@@ -0,0 +1,203 @@
1
+ # Maintenance Guide
2
+
3
+ ## Commands
4
+
5
+ | Command | Description |
6
+ | ------------------------------------------- | ---------------------- |
7
+ | `yarn build --scope @markuplint/vue-parser` | Build this package |
8
+ | `yarn dev --scope @markuplint/vue-parser` | Watch mode build |
9
+ | `yarn clean --scope @markuplint/vue-parser` | Remove build artifacts |
10
+ | `yarn test --scope @markuplint/vue-parser` | Run tests |
11
+
12
+ ## Testing
13
+
14
+ Test files follow the `*.spec.ts` naming convention and are located in the `src/` directory:
15
+
16
+ | Test File | Coverage |
17
+ | --------------- | -------------------------------------------------------------------------------------- |
18
+ | `index.spec.ts` | VueParser integration tests (parsing, directives, namespaces, element types, comments) |
19
+
20
+ The primary testing pattern uses `nodeListToDebugMaps` for snapshot-style assertions:
21
+
22
+ ```ts
23
+ import { nodeListToDebugMaps } from '@markuplint/parser-utils';
24
+ import { parser } from '@markuplint/vue-parser';
25
+
26
+ const doc = parser.parse('<template><div class="foo">text</div></template>');
27
+ const debugMaps = nodeListToDebugMaps(doc.nodeList, true);
28
+ expect(debugMaps).toStrictEqual([
29
+ // expected debug output
30
+ ]);
31
+ ```
32
+
33
+ To test attribute metadata (directives, potentialName, isDynamicValue):
34
+
35
+ ```ts
36
+ const doc = parser.parse('<template><div v-bind:title="val"></div></template>');
37
+ expect(doc.nodeList[0].attributes[0].potentialName).toBe('title');
38
+ expect(doc.nodeList[0].attributes[0].isDynamicValue).toBeTruthy();
39
+ ```
40
+
41
+ To test element types:
42
+
43
+ ```ts
44
+ const doc = parser.parse('<template><MyComponent/></template>');
45
+ expect(doc.nodeList[0].elementType).toBe('authored');
46
+ ```
47
+
48
+ ## Recipes
49
+
50
+ ### 1. Adding or Modifying a Vue Directive
51
+
52
+ 1. Read `src/parser.ts` and find the `visitAttr()` method
53
+ 2. Identify the correct position in the priority chain:
54
+ - `v-on` / `@` (event binding) — first
55
+ - `v-bind` / `:` (property binding) — second
56
+ - `v-model` — third
57
+ - `v-slot` / `#` — fourth
58
+ - Generic `v-*` — last (catch-all)
59
+ 3. Create a new scoped block with a regex pattern:
60
+ ```ts
61
+ {
62
+ const [, directive, name] = attr.name.raw.match(/^(v-newdir:|shorthand)(.+)$/i) ?? [];
63
+ if (directive && name) {
64
+ return {
65
+ ...attr,
66
+ potentialName: name, // if it maps to an HTML attribute
67
+ isDirective: true as const, // if it's Vue-only
68
+ isDynamicValue: true as const, // if value is JavaScript
69
+ };
70
+ }
71
+ }
72
+ ```
73
+ 4. Build: `yarn build --scope @markuplint/vue-parser`
74
+ 5. Add tests to `src/index.spec.ts`:
75
+ - Full form: `v-newdir:value`
76
+ - Shorthand form if applicable
77
+ - With modifiers if applicable
78
+ 6. Test: `yarn test --scope @markuplint/vue-parser`
79
+
80
+ ### 2. Modifying Element Type Detection
81
+
82
+ 1. Read `src/parser.ts` and find the `detectElementType()` method
83
+ 2. The matcher array supports:
84
+ - **String literals** — exact match (e.g., `'Transition'`, `'component'`, `'slot'`)
85
+ - **RegExp** — pattern match (e.g., `/^[A-Z]/` for PascalCase)
86
+ 3. To add a new Vue built-in component:
87
+ ```ts
88
+ detectElementType(nodeName: string) {
89
+ return super.detectElementType(nodeName, [
90
+ // Built-in components
91
+ 'Transition',
92
+ 'TransitionGroup',
93
+ 'KeepAlive',
94
+ 'Teleport',
95
+ 'Suspense',
96
+ 'NewBuiltIn', // <-- add here
97
+ // Special elements
98
+ 'component',
99
+ 'slot',
100
+ // Backward compatibility
101
+ /^[A-Z]/,
102
+ ]);
103
+ }
104
+ ```
105
+ 4. Build and test: `yarn build --scope @markuplint/vue-parser && yarn test --scope @markuplint/vue-parser`
106
+ 5. Add test cases to the `elementType` test block in `src/index.spec.ts`
107
+
108
+ ### 3. Updating vue-eslint-parser Version Support
109
+
110
+ 1. Read `src/vue-parser/index.ts` — this is the wrapper around vue-eslint-parser
111
+ 2. Check the vue-eslint-parser release notes for breaking changes
112
+ 3. Key integration points:
113
+ - `VueESLintParser.parse(vueTemplate, { parser: false })` — main parse call
114
+ - `ast.templateBody?.children` — template child nodes
115
+ - `ast.templateBody?.comments` — template comments
116
+ - `VueESLintParser.AST.VElement` / `VText` / `VExpressionContainer` — node types
117
+ 4. Update type exports if AST types changed:
118
+ ```ts
119
+ export type ASTNode =
120
+ | VueESLintParser.AST.VElement
121
+ | VueESLintParser.AST.VText
122
+ | VueESLintParser.AST.VExpressionContainer;
123
+ ```
124
+ 5. Build and test: `yarn build --scope @markuplint/vue-parser && yarn test --scope @markuplint/vue-parser`
125
+
126
+ ### 4. Fixing Template Comment Injection
127
+
128
+ 1. Read the `flattenNodes()` method in `src/parser.ts`
129
+ 2. The comment injection logic:
130
+ - Comments are stored in `this.state.comments` during `tokenize()`
131
+ - During flattening, for each pair of adjacent nodes, the method checks if a comment falls between them
132
+ - The range check: `lastOffset <= comment.range[0] && comment.range[1] <= node.startOffset`
133
+ 3. Common issues:
134
+ - Comment not injected: check that the range check covers the gap correctly
135
+ - Comment at wrong position: check `lastOffset` calculation (`prevNode?.endOffset ?? node.parentNode?.endOffset ?? 0`)
136
+ - Bogus comment detection: verify `betweenComment.type === 'HTMLBogusComment'`
137
+ 4. Build and test: `yarn build --scope @markuplint/vue-parser && yarn test --scope @markuplint/vue-parser`
138
+
139
+ ## Upstream Impact Checklist
140
+
141
+ Changes to upstream packages can affect this package:
142
+
143
+ | Package | Impact |
144
+ | -------------------------- | ----------------------------------------------------------------------------- |
145
+ | `@markuplint/parser-utils` | Base `Parser` class changes may affect all override methods |
146
+ | `@markuplint/ml-ast` | AST type changes may require updates to nodeize() return types |
147
+ | `vue-eslint-parser` | AST structure changes may require updates to tokenize(), nodeize(), and types |
148
+
149
+ When upstream packages change, run:
150
+
151
+ ```shell
152
+ yarn test --scope @markuplint/vue-parser
153
+ ```
154
+
155
+ ## Troubleshooting
156
+
157
+ ### Vue directive is not recognized
158
+
159
+ **Symptom:** A Vue directive like `v-custom` is treated as a regular HTML attribute instead of being marked as `isDirective: true`.
160
+
161
+ **Cause:** The directive pattern does not match in `visitAttr()`, or the new directive block is placed after the generic `v-*` catch-all.
162
+
163
+ **Solution:**
164
+
165
+ 1. Check the regex pattern in your directive block — ensure it matches the full attribute name
166
+ 2. Ensure the block is placed before the generic `v-*` catch-all at the end of `visitAttr()`
167
+ 3. Verify the return object includes `isDirective: true as const`
168
+
169
+ ### Template comments are missing from the AST
170
+
171
+ **Symptom:** HTML comments (`<!-- ... -->`) in the Vue template are not present in the parsed node list.
172
+
173
+ **Cause:** The comments are not being injected during `flattenNodes()`, or they are not captured during `tokenize()`.
174
+
175
+ **Solution:**
176
+
177
+ 1. Check `tokenize()` — verify `ast.templateBody?.comments` is being stored in `this.state.comments`
178
+ 2. Check `flattenNodes()` — verify the range check logic finds the comment between adjacent nodes
179
+ 3. Add a test case with the specific comment position and check `nodeListToDebugMaps` output
180
+
181
+ ### PascalCase component not detected as 'authored'
182
+
183
+ **Symptom:** A component like `<MyComponent>` has `elementType: 'html'` instead of `'authored'`.
184
+
185
+ **Cause:** The `/^[A-Z]/` regex in `detectElementType()` is not matching, or the matcher array is misconfigured.
186
+
187
+ **Solution:**
188
+
189
+ 1. Check that the component name starts with an uppercase letter
190
+ 2. Verify the `/^[A-Z]/` regex is still present in the matcher array
191
+ 3. Note that lowercase Vue built-ins like `<component>` and `<slot>` are matched by string, not regex
192
+
193
+ ### SyntaxError not properly reported
194
+
195
+ **Symptom:** Vue template syntax errors crash the process instead of producing a `ParserError`.
196
+
197
+ **Cause:** The `parseError()` method is not catching the error, or the error object does not have `lineNumber`/`column` properties.
198
+
199
+ **Solution:**
200
+
201
+ 1. Check `parseError()` — verify the `instanceof SyntaxError` and `'lineNumber' in error` checks
202
+ 2. vue-eslint-parser errors include `lineNumber` (1-based) and `column` (0-based)
203
+ 3. The fallback `super.parseError(error)` handles non-SyntaxError cases
package/lib/index.d.ts CHANGED
@@ -1 +1,7 @@
1
+ /**
2
+ * @module
3
+ * Vue Single File Component (SFC) template parser for markuplint. Provides a parser
4
+ * that transforms Vue template syntax into markuplint's AST using the vue-eslint-parser,
5
+ * handling Vue-specific directives such as `v-bind`, `v-on`, `v-model`, and `v-slot`.
6
+ */
1
7
  export { parser } from './parser.js';
package/lib/index.js CHANGED
@@ -1 +1,7 @@
1
+ /**
2
+ * @module
3
+ * Vue Single File Component (SFC) template parser for markuplint. Provides a parser
4
+ * that transforms Vue template syntax into markuplint's AST using the vue-eslint-parser,
5
+ * handling Vue-specific directives such as `v-bind`, `v-on`, `v-model`, and `v-slot`.
6
+ */
1
7
  export { parser } from './parser.js';
package/lib/parser.d.ts CHANGED
@@ -5,6 +5,12 @@ import { ParserError, Parser } from '@markuplint/parser-utils';
5
5
  type State = {
6
6
  comments: readonly ASTComment[];
7
7
  };
8
+ /**
9
+ * Parser implementation for Vue SFC templates.
10
+ * Extends the base Parser to handle Vue elements, text nodes, expression containers,
11
+ * directives (`v-bind`, `v-on`, `v-model`, `v-slot`), and template comments.
12
+ * Recognizes Vue built-in components and PascalCase user components.
13
+ */
8
14
  declare class VueParser extends Parser<ASTNode, State> {
9
15
  readonly duplicatableAttrs: Set<string>;
10
16
  constructor();
@@ -13,9 +19,35 @@ declare class VueParser extends Parser<ASTNode, State> {
13
19
  isFragment: boolean;
14
20
  };
15
21
  parseError(error: any): ParserError;
22
+ /**
23
+ * Converts a Vue AST node into markuplint node tree items.
24
+ * Handles VText (text nodes), VExpressionContainer (template expressions),
25
+ * and VElement (elements with start/end tags and children).
26
+ *
27
+ * @param originNode - The Vue AST node to convert
28
+ * @param parentNode - The parent node in the markuplint tree, or null for root nodes
29
+ * @param depth - The nesting depth of the node
30
+ * @returns An array of markuplint node tree items
31
+ */
16
32
  nodeize(originNode: ASTNode, parentNode: MLASTParentNode | null, depth: number): readonly MLASTNodeTreeItem[];
33
+ /**
34
+ * Extends the base flattening to inject Vue template comments between sibling nodes.
35
+ * Comments from the vue-eslint-parser are inserted at the correct positions
36
+ * based on their source offsets relative to adjacent nodes.
37
+ *
38
+ * @param nodeTree - The hierarchical node tree to flatten
39
+ * @returns A flat list of nodes including interleaved comments
40
+ */
17
41
  flattenNodes(nodeTree: readonly MLASTNodeTreeItem[]): MLASTNodeTreeItem[];
18
42
  afterFlattenNodes(nodeList: readonly MLASTNodeTreeItem[]): readonly MLASTNodeTreeItem[];
43
+ /**
44
+ * Visits an attribute token and resolves Vue-specific directive shorthands.
45
+ * Handles `v-on` / `@` (event binding), `v-bind` / `:` (property binding),
46
+ * `v-model`, `v-slot` / `#`, and other `v-` prefixed directives.
47
+ *
48
+ * @param token - The token representing the attribute
49
+ * @returns The parsed attribute node with Vue-specific metadata
50
+ */
19
51
  visitAttr(token: Token): (import("@markuplint/ml-ast").MLASTHTMLAttr & {
20
52
  __rightText?: string;
21
53
  }) | (import("@markuplint/ml-ast").MLASTSpreadAttr & {
package/lib/parser.js CHANGED
@@ -1,5 +1,11 @@
1
1
  import { ParserError, Parser } from '@markuplint/parser-utils';
2
2
  import { vueParse } from './vue-parser/index.js';
3
+ /**
4
+ * Parser implementation for Vue SFC templates.
5
+ * Extends the base Parser to handle Vue elements, text nodes, expression containers,
6
+ * directives (`v-bind`, `v-on`, `v-model`, `v-slot`), and template comments.
7
+ * Recognizes Vue built-in components and PascalCase user components.
8
+ */
3
9
  class VueParser extends Parser {
4
10
  constructor() {
5
11
  super({
@@ -29,6 +35,16 @@ class VueParser extends Parser {
29
35
  }
30
36
  return super.parseError(error);
31
37
  }
38
+ /**
39
+ * Converts a Vue AST node into markuplint node tree items.
40
+ * Handles VText (text nodes), VExpressionContainer (template expressions),
41
+ * and VElement (elements with start/end tags and children).
42
+ *
43
+ * @param originNode - The Vue AST node to convert
44
+ * @param parentNode - The parent node in the markuplint tree, or null for root nodes
45
+ * @param depth - The nesting depth of the node
46
+ * @returns An array of markuplint node tree items
47
+ */
32
48
  nodeize(
33
49
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
34
50
  originNode, parentNode, depth) {
@@ -74,6 +90,14 @@ class VueParser extends Parser {
74
90
  }
75
91
  }
76
92
  }
93
+ /**
94
+ * Extends the base flattening to inject Vue template comments between sibling nodes.
95
+ * Comments from the vue-eslint-parser are inserted at the correct positions
96
+ * based on their source offsets relative to adjacent nodes.
97
+ *
98
+ * @param nodeTree - The hierarchical node tree to flatten
99
+ * @returns A flat list of nodes including interleaved comments
100
+ */
77
101
  flattenNodes(nodeTree) {
78
102
  const nodeList = super.flattenNodes(nodeTree);
79
103
  const newNodeList = [];
@@ -111,6 +135,14 @@ class VueParser extends Parser {
111
135
  concatText: false,
112
136
  });
113
137
  }
138
+ /**
139
+ * Visits an attribute token and resolves Vue-specific directive shorthands.
140
+ * Handles `v-on` / `@` (event binding), `v-bind` / `:` (property binding),
141
+ * `v-model`, `v-slot` / `#`, and other `v-` prefixed directives.
142
+ *
143
+ * @param token - The token representing the attribute
144
+ * @returns The parsed attribute node with Vue-specific metadata
145
+ */
114
146
  visitAttr(token) {
115
147
  const attr = super.visitAttr(token);
116
148
  if (attr.type === 'spread') {
@@ -1,5 +1,14 @@
1
1
  import * as VueESLintParser from 'vue-eslint-parser';
2
+ /** The top-level AST produced by vue-eslint-parser, containing template body and comments. */
2
3
  export type VueTokens = VueESLintParser.AST.ESLintProgram;
4
+ /**
5
+ * Parses a Vue SFC template string into a vue-eslint-parser AST.
6
+ *
7
+ * @param vueTemplate - The raw Vue template source code
8
+ * @returns The parsed AST program node containing the template body
9
+ */
3
10
  export declare function vueParse(vueTemplate: string): VueTokens;
11
+ /** Union of AST node types that can appear as children in a Vue template. */
4
12
  export type ASTNode = VueESLintParser.AST.VElement | VueESLintParser.AST.VText | VueESLintParser.AST.VExpressionContainer;
13
+ /** Represents a comment token in the Vue template AST with location information. */
5
14
  export type ASTComment = VueESLintParser.AST.Token & VueESLintParser.AST.HasLocation;
@@ -1,4 +1,10 @@
1
1
  import * as VueESLintParser from 'vue-eslint-parser';
2
+ /**
3
+ * Parses a Vue SFC template string into a vue-eslint-parser AST.
4
+ *
5
+ * @param vueTemplate - The raw Vue template source code
6
+ * @returns The parsed AST program node containing the template body
7
+ */
2
8
  export function vueParse(vueTemplate) {
3
9
  const ast = VueESLintParser.parse(vueTemplate, { parser: false });
4
10
  return ast;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/vue-parser",
3
- "version": "4.6.21",
3
+ "version": "4.6.23",
4
4
  "description": "Vue parser for markuplint",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
@@ -21,10 +21,10 @@
21
21
  "clean": "tsc --build --clean tsconfig.build.json"
22
22
  },
23
23
  "dependencies": {
24
- "@markuplint/html-parser": "4.6.21",
25
- "@markuplint/ml-ast": "4.4.10",
26
- "@markuplint/parser-utils": "4.8.9",
24
+ "@markuplint/html-parser": "4.6.23",
25
+ "@markuplint/ml-ast": "4.4.11",
26
+ "@markuplint/parser-utils": "4.8.11",
27
27
  "vue-eslint-parser": "10.2.0"
28
28
  },
29
- "gitHead": "ae97eb2d31ecedf4f0800fbbf18588aad4ebca04"
29
+ "gitHead": "193ee7c1262bbed95424e38efdf1a8e56ff049f4"
30
30
  }