@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.
- package/ARCHITECTURE.ja.md +283 -0
- package/ARCHITECTURE.md +283 -0
- package/CHANGELOG.md +5 -1
- package/SKILL.md +137 -0
- package/docs/maintenance.ja.md +203 -0
- package/docs/maintenance.md +203 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/parser.d.ts +32 -0
- package/lib/parser.js +32 -0
- package/lib/vue-parser/index.d.ts +9 -0
- package/lib/vue-parser/index.js +6 -0
- package/package.json +5 -5
|
@@ -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) -- コマンド、レシピ、トラブルシューティング
|
package/ARCHITECTURE.md
ADDED
|
@@ -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.
|
|
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;
|
package/lib/vue-parser/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
25
|
-
"@markuplint/ml-ast": "4.4.
|
|
26
|
-
"@markuplint/parser-utils": "4.8.
|
|
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": "
|
|
29
|
+
"gitHead": "193ee7c1262bbed95424e38efdf1a8e56ff049f4"
|
|
30
30
|
}
|