@markuplint/vue-parser 5.0.0-rc.4 → 5.0.0-rc.5

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/CHANGELOG.md CHANGED
@@ -3,6 +3,10 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.0.0-rc.5](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.4...v5.0.0-rc.5) (2026-08-28)
7
+
8
+ **Note:** Version bump only for package @markuplint/vue-parser
9
+
6
10
  # [5.0.0-rc.4](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.3...v5.0.0-rc.4) (2026-04-19)
7
11
 
8
12
  **Note:** Version bump only for package @markuplint/vue-parser
@@ -1,7 +1,4 @@
1
1
  import { parser } from './parser.js';
2
- /**
3
- * Extracts root element information from a parsed MLAST document.
4
- */
5
2
  function extractComponentInfo(doc) {
6
3
  const root = doc.nodeList.find((n) => n.type === 'starttag' && n.depth === 0 && !n.isFragment);
7
4
  if (!root) {
@@ -28,15 +25,9 @@ function extractComponentInfo(doc) {
28
25
  col: root.col,
29
26
  };
30
27
  }
31
- /**
32
- * Detects whether the parsed Vue template contains `<slot>` elements.
33
- */
34
28
  function detectSlots(doc) {
35
29
  return doc.nodeList.some(n => n.type === 'starttag' && n.nodeName === 'slot');
36
30
  }
37
- /**
38
- * Extracts the `<script setup>` block from a Vue SFC source.
39
- */
40
31
  function extractVueScriptSetup(source) {
41
32
  const re = /<script\s[^>]*?\bsetup\b[^>]*>/i;
42
33
  const match = re.exec(source);
package/lib/parser.d.ts CHANGED
@@ -5,10 +5,25 @@ type State = {
5
5
  comments: readonly ASTComment[];
6
6
  };
7
7
  /**
8
- * Parser implementation for Vue SFC templates.
9
- * Extends the base Parser to handle Vue elements, text nodes, expression containers,
10
- * directives (`v-bind`, `v-on`, `v-model`, `v-slot`), and template comments.
11
- * Recognizes Vue built-in components and PascalCase user components.
8
+ * Directive resolution is intentionally not performed by this parser:
9
+ * `directivePatterns` declared in `@markuplint/vue-spec` are applied later by
10
+ * `ml-core`'s `MLAttr` constructor. Consequently, parser-level output (and
11
+ * `index.spec.ts`) shows unresolved attribute metadata, while the final
12
+ * `potentialName`/`isDirective`/`isDynamicValue` values only appear at the
13
+ * core level.
14
+ *
15
+ * Known limitation: `v-if`/`v-for`/`v-else`/`v-else-if` do not set
16
+ * `blockBehavior`, so content-model rules such as `permitted-contents` cannot
17
+ * enumerate conditional branches via `conditionalChildNodes()` as they can for
18
+ * Svelte, Pug, Alpine, JSX, and Astro. Unlike Alpine's fixed
19
+ * `<template x-if="...">` wrapper, which converts cleanly into a PSBlock, Vue
20
+ * directives attach to arbitrary elements that must remain valid HTML elements
21
+ * for attribute validation while also acting as blocks for content-model
22
+ * analysis, and `v-else`/`v-else-if` branch across sibling elements — both
23
+ * beyond the per-node `nodeize()` model. These directives are handled at the
24
+ * attribute level only (`isDirective: true`), which suppresses attribute
25
+ * validation errors but provides no structural block information to the core
26
+ * engine.
12
27
  */
13
28
  declare class VueParser extends Parser<ASTNode, State> {
14
29
  constructor();
@@ -17,24 +32,11 @@ declare class VueParser extends Parser<ASTNode, State> {
17
32
  readonly isFragment: boolean;
18
33
  };
19
34
  parseError(error: any): ParserError;
20
- /**
21
- * Converts a Vue AST node into markuplint node tree items.
22
- * Handles VText (text nodes), VExpressionContainer (template expressions),
23
- * and VElement (elements with start/end tags and children).
24
- *
25
- * @param originNode - The Vue AST node to convert
26
- * @param parentNode - The parent node in the markuplint tree, or null for root nodes
27
- * @param depth - The nesting depth of the node
28
- * @returns An array of markuplint node tree items
29
- */
30
35
  nodeize(originNode: ASTNode, parentNode: MLASTParentNode | null, depth: number): readonly MLASTNodeTreeItem[];
31
36
  /**
32
- * Extends the base flattening to inject Vue template comments between sibling nodes.
33
- * Comments from the vue-eslint-parser are inserted at the correct positions
34
- * based on their source offsets relative to adjacent nodes.
35
- *
36
- * @param nodeTree - The hierarchical node tree to flatten
37
- * @returns A flat list of nodes including interleaved comments
37
+ * This second pass that interleaves comments is required because
38
+ * vue-eslint-parser provides comments separately (`templateBody.comments`)
39
+ * from the main node tree, so they cannot be emitted during `nodeize()`.
38
40
  */
39
41
  flattenNodes(nodeTree: readonly MLASTNodeTreeItem[]): MLASTNodeTreeItem[];
40
42
  afterFlattenNodes(nodeList: readonly MLASTNodeTreeItem[]): readonly MLASTNodeTreeItem[];
@@ -48,8 +50,6 @@ declare class VueParser extends Parser<ASTNode, State> {
48
50
  * @see https://vuejs.org/guide/essentials/component-basics#using-a-component
49
51
  * @see https://vuejs.org/api/built-in-components.html
50
52
  * @see https://vuejs.org/api/built-in-special-elements.html#built-in-special-elements
51
- * @param nodeName
52
- * @returns
53
53
  */
54
54
  detectElementType(nodeName: string): import("@markuplint/ml-ast").ElementType;
55
55
  }
package/lib/parser.js CHANGED
@@ -1,14 +1,31 @@
1
1
  import { ParserError, Parser } from '@markuplint/parser-utils';
2
2
  import { vueParse } from './vue-parser/index.js';
3
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.
4
+ * Directive resolution is intentionally not performed by this parser:
5
+ * `directivePatterns` declared in `@markuplint/vue-spec` are applied later by
6
+ * `ml-core`'s `MLAttr` constructor. Consequently, parser-level output (and
7
+ * `index.spec.ts`) shows unresolved attribute metadata, while the final
8
+ * `potentialName`/`isDirective`/`isDynamicValue` values only appear at the
9
+ * core level.
10
+ *
11
+ * Known limitation: `v-if`/`v-for`/`v-else`/`v-else-if` do not set
12
+ * `blockBehavior`, so content-model rules such as `permitted-contents` cannot
13
+ * enumerate conditional branches via `conditionalChildNodes()` as they can for
14
+ * Svelte, Pug, Alpine, JSX, and Astro. Unlike Alpine's fixed
15
+ * `<template x-if="...">` wrapper, which converts cleanly into a PSBlock, Vue
16
+ * directives attach to arbitrary elements that must remain valid HTML elements
17
+ * for attribute validation while also acting as blocks for content-model
18
+ * analysis, and `v-else`/`v-else-if` branch across sibling elements — both
19
+ * beyond the per-node `nodeize()` model. These directives are handled at the
20
+ * attribute level only (`isDirective: true`), which suppresses attribute
21
+ * validation errors but provides no structural block information to the core
22
+ * engine.
8
23
  */
9
24
  class VueParser extends Parser {
10
25
  constructor() {
11
26
  super({
27
+ // Vue SFC is a compiled format that uses explicit XML-style
28
+ // closing tags (including `/>`), not HTML void-element rules.
12
29
  endTagType: 'xml',
13
30
  }, {
14
31
  comments: [],
@@ -25,6 +42,8 @@ class VueParser extends Parser {
25
42
  };
26
43
  }
27
44
  parseError(error) {
45
+ // vue-eslint-parser syntax errors carry `lineNumber` (1-based) and
46
+ // `column` (0-based); non-SyntaxError cases fall back to the base handler.
28
47
  if (error instanceof SyntaxError && 'lineNumber' in error && 'column' in error) {
29
48
  throw new ParserError(error.message, {
30
49
  line: error.lineNumber,
@@ -34,16 +53,6 @@ class VueParser extends Parser {
34
53
  }
35
54
  return super.parseError(error);
36
55
  }
37
- /**
38
- * Converts a Vue AST node into markuplint node tree items.
39
- * Handles VText (text nodes), VExpressionContainer (template expressions),
40
- * and VElement (elements with start/end tags and children).
41
- *
42
- * @param originNode - The Vue AST node to convert
43
- * @param parentNode - The parent node in the markuplint tree, or null for root nodes
44
- * @param depth - The nesting depth of the node
45
- * @returns An array of markuplint node tree items
46
- */
47
56
  nodeize(
48
57
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
49
58
  originNode, parentNode, depth) {
@@ -57,6 +66,8 @@ class VueParser extends Parser {
57
66
  });
58
67
  }
59
68
  case 'VExpressionContainer': {
69
+ // Template expressions (`{{ ... }}`) are treated as opaque
70
+ // pseudo-blocks; their JavaScript content is intentionally not parsed.
60
71
  return this.visitPsBlock({
61
72
  ...token,
62
73
  depth,
@@ -89,12 +100,9 @@ class VueParser extends Parser {
89
100
  }
90
101
  }
91
102
  /**
92
- * Extends the base flattening to inject Vue template comments between sibling nodes.
93
- * Comments from the vue-eslint-parser are inserted at the correct positions
94
- * based on their source offsets relative to adjacent nodes.
95
- *
96
- * @param nodeTree - The hierarchical node tree to flatten
97
- * @returns A flat list of nodes including interleaved comments
103
+ * This second pass that interleaves comments is required because
104
+ * vue-eslint-parser provides comments separately (`templateBody.comments`)
105
+ * from the main node tree, so they cannot be emitted during `nodeize()`.
98
106
  */
99
107
  flattenNodes(nodeTree) {
100
108
  const nodeList = super.flattenNodes(nodeTree);
@@ -131,6 +139,8 @@ class VueParser extends Parser {
131
139
  return newNodeList;
132
140
  }
133
141
  afterFlattenNodes(nodeList) {
142
+ // All base post-processing is disabled because Vue's template parser
143
+ // handles whitespace and node validity differently from raw HTML parsing.
134
144
  return super.afterFlattenNodes(nodeList, {
135
145
  exposeInvalidNode: false,
136
146
  exposeWhiteSpace: false,
@@ -147,8 +157,6 @@ class VueParser extends Parser {
147
157
  * @see https://vuejs.org/guide/essentials/component-basics#using-a-component
148
158
  * @see https://vuejs.org/api/built-in-components.html
149
159
  * @see https://vuejs.org/api/built-in-special-elements.html#built-in-special-elements
150
- * @param nodeName
151
- * @returns
152
160
  */
153
161
  detectElementType(nodeName) {
154
162
  return super.detectElementType(nodeName, [
@@ -1,17 +1,15 @@
1
1
  import * as VueESLintParser from 'vue-eslint-parser';
2
- /** The top-level AST produced by vue-eslint-parser, containing template body and comments. */
3
2
  export type VueTokens = VueESLintParser.AST.ESLintProgram;
4
3
  /**
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
4
+ * The `parser: false` option makes vue-eslint-parser skip parsing the
5
+ * `<script>` block, because only the `<template>` block is relevant for
6
+ * markuplint. vue-eslint-parser accepts both Vue 2 and Vue 3 template
7
+ * syntax and produces the same node types for both, so the parser does not
8
+ * need to distinguish Vue versions at the AST level.
9
9
  */
10
10
  export declare function vueParse(vueTemplate: string): VueTokens;
11
11
  export type VElement = VueESLintParser.AST.VElement;
12
12
  export type VText = VueESLintParser.AST.VText;
13
13
  export type VExpressionContainer = VueESLintParser.AST.VExpressionContainer;
14
- /** Union of AST node types that can appear as children in a Vue template. */
15
14
  export type ASTNode = VElement | VText | VExpressionContainer;
16
- /** Represents a comment token in the Vue template AST with location information. */
17
15
  export type ASTComment = VueESLintParser.AST.Token & VueESLintParser.AST.HasLocation;
@@ -1,9 +1,10 @@
1
1
  import * as VueESLintParser from 'vue-eslint-parser';
2
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
3
+ * The `parser: false` option makes vue-eslint-parser skip parsing the
4
+ * `<script>` block, because only the `<template>` block is relevant for
5
+ * markuplint. vue-eslint-parser accepts both Vue 2 and Vue 3 template
6
+ * syntax and produces the same node types for both, so the parser does not
7
+ * need to distinguish Vue versions at the AST level.
7
8
  */
8
9
  export function vueParse(vueTemplate) {
9
10
  const ast = VueESLintParser.parse(vueTemplate, { parser: false });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/vue-parser",
3
- "version": "5.0.0-rc.4",
3
+ "version": "5.0.0-rc.5",
4
4
  "description": "Vue parser for markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,7 @@
10
10
  "author": "Yusuke Hirao <yusukehirao@me.com>",
11
11
  "license": "MIT",
12
12
  "engines": {
13
- "node": ">=22"
13
+ "node": ">=24"
14
14
  },
15
15
  "type": "module",
16
16
  "exports": {
@@ -32,10 +32,10 @@
32
32
  "clean": "tsc --build --clean tsconfig.build.json"
33
33
  },
34
34
  "dependencies": {
35
- "@markuplint/html-parser": "5.0.0-rc.4",
36
- "@markuplint/ml-ast": "5.0.0-rc.4",
37
- "@markuplint/parser-utils": "5.0.0-rc.4",
35
+ "@markuplint/html-parser": "5.0.0-rc.5",
36
+ "@markuplint/ml-ast": "5.0.0-rc.5",
37
+ "@markuplint/parser-utils": "5.0.0-rc.5",
38
38
  "vue-eslint-parser": "10.4.0"
39
39
  },
40
- "gitHead": "97a6339bbae23f556de5d307b3ce2ef7cfd9402d"
40
+ "gitHead": "8d87463af2ff3f1b83fb28da20f1819362cf3555"
41
41
  }
@@ -1,314 +0,0 @@
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
- ├── component-scanner.ts — pretenders 自動スキャン用コンポーネントスキャナー(サブパスエクスポート)
14
- ├── index.spec.ts — VueParser の統合テスト
15
- ├── component-scanner.spec.ts — コンポーネントスキャナーのテスト
16
- └── vue-parser/
17
- └── index.ts — vue-eslint-parser ラッパー、ASTNode/ASTComment 型エクスポート
18
- ```
19
-
20
- ## アーキテクチャ図
21
-
22
- ```mermaid
23
- flowchart TD
24
- subgraph upstream ["上流"]
25
- mlAst["@markuplint/ml-ast\n(AST 型定義)"]
26
- parserUtils["@markuplint/parser-utils\n(抽象 Parser クラス)"]
27
- vueEslintParser["vue-eslint-parser\n(Vue SFC トークナイザ)"]
28
- end
29
-
30
- subgraph pkg ["@markuplint/vue-parser"]
31
- vueParser["VueParser\nextends Parser‹ASTNode, State›"]
32
- vueParseFn["vueParse()\nvue-eslint-parser ラッパー"]
33
- compScanner["componentScanner\n(サブパス: ./component-scanner)"]
34
- end
35
-
36
- subgraph downstream ["下流"]
37
- mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
38
- pretenders["@markuplint/pretenders\n(自動スキャン)"]
39
- end
40
-
41
- mlAst -->|"AST 型"| vueParser
42
- parserUtils -->|"Parser 基底クラス"| vueParser
43
- vueEslintParser -->|"parse()"| vueParseFn
44
- vueParseFn -->|"ESLintProgram AST"| vueParser
45
-
46
- vueParser -->|"MLASTDocument を生成"| mlCore
47
- vueParser -->|"parse()"| compScanner
48
- compScanner -->|"ComponentScanResult"| pretenders
49
- ```
50
-
51
- ## VueParser クラス
52
-
53
- ### 継承関係
54
-
55
- ```
56
- Parser<ASTNode, State> (@markuplint/parser-utils)
57
- └── VueParser (このパッケージ)
58
- ```
59
-
60
- ### コンストラクタ
61
-
62
- コンストラクタは2つの引数でパーサーを構成します:
63
-
64
- | 引数 | 値 | 用途 |
65
- | --------------- | ----------------------- | ---------------------------------------------------------------------------------- |
66
- | `ParserOptions` | `{ endTagType: 'xml' }` | Vue テンプレートは明示的な閉じタグを使用(XML スタイル)、HTML void ルールではない |
67
- | 初期 State | `{ comments: [] }` | 空のコメント配列。`tokenize()` で設定される |
68
-
69
- `tagNameCaseSensitive` の動作は基底クラスから継承され、Vue の `detectElementType` オーバーライドと組み合わせて PascalCase コンポーネント名を正しく処理します。
70
-
71
- ### State 型
72
-
73
- パーサーは `State` 型を通じて内部状態を管理します:
74
-
75
- | フィールド | 型 | 用途 |
76
- | ---------- | ----------------------- | -------------------------------------------------------------------------------------------- |
77
- | `comments` | `readonly ASTComment[]` | tokenize 中に vue-eslint-parser から抽出されたテンプレートコメント。後の flattenNodes で注入 |
78
-
79
- ### オーバーライドメソッド
80
-
81
- | メソッド | 用途 |
82
- | --------------------- | --------------------------------------------------------------------------------------------- |
83
- | `tokenize()` | vue-eslint-parser を呼び出し、`templateBody.children` とコメントを抽出 |
84
- | `parseError()` | vue-eslint-parser の `SyntaxError`(`lineNumber`/`column` 付き)を `ParserError` に変換 |
85
- | `nodeize()` | vue-eslint-parser AST ノード(VText、VElement、VExpressionContainer)を markuplint AST に変換 |
86
- | `flattenNodes()` | 基底のフラット化を拡張し、兄弟ノード間にテンプレートコメントを注入 |
87
- | `afterFlattenNodes()` | `exposeWhiteSpace: false`、`exposeInvalidNode: false`、`concatText: false` で基底を呼び出す |
88
- | `detectElementType()` | PascalCase コンポーネントと Vue 組み込みコンポーネントを検出 |
89
-
90
- > **注記:** `visitAttr()` オーバーライドは削除されました。Vue ディレクティブ処理(`v-bind`、`v-on`、`v-model`、`v-slot` など)は `@markuplint/vue-spec` の `directivePatterns` で管理されています。
91
-
92
- ### `duplicatableAttrs`
93
-
94
- `'class'` と `'style'` を含む `Set<string>` -- `v-bind:class` と `class` が同一要素に共存できるように、重複可能な属性を定義します。
95
-
96
- ## tokenize()
97
-
98
- `tokenize()` メソッドは vue-eslint-parser AST を取得するエントリーポイントです:
99
-
100
- 1. `vueParse(this.rawCode)` を呼び出し、内部で `VueESLintParser.parse(vueTemplate, { parser: false })` を実行
101
- 2. `ast.templateBody?.comments` が存在する場合、`this.state.comments` に格納して後の注入に備える
102
- 3. `{ ast: ast.templateBody?.children ?? [], isFragment: true }` を返す
103
-
104
- `parser: false` オプションは vue-eslint-parser に `<script>` のパースをスキップするよう指示します(markuplint にとって関連するのは `<template>` ブロックのみ)。ソースに `<template>` ブロックがないか空の場合、`templateBody?.children` は `undefined` を返し、パーサーは空の配列を受け取ります。
105
-
106
- ## nodeize() の詳細
107
-
108
- `nodeize()` メソッドは `originNode.type` フィールドに基づいてディスパッチします:
109
-
110
- ### VText -> visitText
111
-
112
- テキストノードは `this.sliceFragment(range[0], range[1])` でソースからスライスされ、depth と parentNode と共に基底の `visitText()` メソッドに渡されます。
113
-
114
- ### VExpressionContainer -> visitPsBlock
115
-
116
- `{{ expression }}` のような式コンテナは `visitPsBlock()` で擬似ブロックノードに変換されます:
117
-
118
- - `nodeName`: `'vue-expression-container'`
119
- - `isFragment`: `false`
120
-
121
- これにより、Vue テンプレート式は JavaScript コンテンツのパースを試みるのではなく、markuplint AST 内で不透明なブロックとして扱われます。
122
-
123
- ### VElement -> visitElement
124
-
125
- 要素ノードの場合、メソッドは:
126
-
127
- 1. `originNode.startTag.range` から**開始タグ**トークンをスライス
128
- 2. 要素の `name` と `namespace` と共に `visitElement()` を呼び出す
129
- 3. `originNode.children` を子ノードとして渡す -- テンプレートルートを表すノードの場合、これは `templateBody.children`
130
- 4. エンドタグトークンファクトリ(`createEndTagToken`)を作成 -- 要素が自己閉じの場合は `null` を返し、そうでなければ `originNode.endTag.range` からスライス
131
-
132
- ## flattenNodes()
133
-
134
- `flattenNodes()` メソッドは基底の `Parser.flattenNodes()` を拡張してテンプレートコメントを注入します:
135
-
136
- 1. `super.flattenNodes(nodeTree)` を呼び出して初期フラットノードリストを取得
137
- 2. ノードリストを走査し、隣接するノードペア間のコメントを確認
138
- 3. `prevNode.endOffset`(最初のノードの場合は `parentNode.endOffset`)と `node.startOffset` の間の各ギャップについて、そのギャップ内に範囲が収まるコメントを `this.state.comments` から検索
139
- 4. コメントが見つかった場合、`this.visitComment()` で作成し、コメントの type に基づいて `isBogus` を設定(`HTMLBogusComment` の場合は true)
140
- 5. `this.appendChild()` でコメントを親ノードに追加
141
-
142
- この2パスアプローチが必要な理由は、vue-eslint-parser がコメントをメインノードツリーとは別に提供するため、正しい位置にインターリーブする必要があるからです。
143
-
144
- ## ディレクティブ処理(@markuplint/vue-spec の directivePatterns)
145
-
146
- Vue ディレクティブの解決はパーサー自体ではなく、`@markuplint/vue-spec` で定義された `directivePatterns` によって管理されています。spec がディレクティブを `potentialName`、`isDirective`、`isDynamicValue` メタデータにマッピングするパターンを宣言します。
147
-
148
- > **二段階解決:** パーサーレベルのテスト(`index.spec.ts`)はパーサー自体が設定する raw AST 値を示します(例: 波括弧式のみ `isDynamicValue: true`)。コアレベルのテスト(`ml-core` や `rules`)は `ml-core` の `MLAttr` コンストラクタが `directivePatterns` を適用した後の最終解決値を示します。例えば、値なしの `on:click` はパーサーレベルでは `isDynamicValue: false` ですが、コアレベルでは `directivePatterns` マッチにより `isDynamicValue: true` に解決されます。
149
-
150
- ### クォートセット
151
-
152
- 基底パーサーは標準 HTML クォート(`"`、`'`)を処理します。Vue テンプレートは式バインディングの暗黙的な値デリミタとして `{}` も使用しますが、属性値自体は標準のクォーティングを使用します。
153
-
154
- ### Vue ディレクティブ処理
155
-
156
- ディレクティブは優先順位に従って処理されます。最初にマッチするパターンが適用されます:
157
-
158
- #### `v-on` / `@`(イベントバインディング)
159
-
160
- - **パターン**: `/^(v-on:|@)([^.]+)(?:\.([^.]+))?$/i`
161
- - **結果**: `potentialName: 'on' + eventName.toLowerCase()`、`isDynamicValue: true`
162
- - **例**:
163
- - `@click` -> `potentialName: 'onclick'`
164
- - `v-on:click.stop` -> `potentialName: 'onclick'`
165
- - `@keydown.enter` -> `potentialName: 'onkeydown'`
166
-
167
- #### `v-bind` / `:`(プロパティバインディング)
168
-
169
- - **パターン**: `/^(v-bind:|:)([^.]+)(?:\.([^.]+))?$/i`
170
- - **結果**(修飾子なし): `potentialName: propName`、`isDynamicValue: true`
171
- - **結果**(`.attr` 修飾子): `potentialName: propName`、`isDynamicValue: true`
172
- - **結果**(`.prop` / `.camel` / その他修飾子): `isDirective: true`、`potentialName` が正規化された形式に設定
173
- - **`isDuplicatable`**: バインドされたプロパティが `duplicatableAttrs` に含まれる場合(class、style)、`isDuplicatable` が `true` に設定
174
- - **例**:
175
- - `:data-attr` -> `potentialName: 'data-attr'`
176
- - `v-bind:class` -> `potentialName: 'class'`、`isDuplicatable: true`
177
- - `:title.attr` -> `potentialName: 'title'`
178
- - `:foo.prop` -> `isDirective: true`
179
-
180
- #### `v-model`
181
-
182
- - **パターン**: `/^(v-model)(?:\.([^.]+))?$/i`
183
- - **結果**: `isDirective: true`
184
- - **例**:
185
- - `v-model` -> `isDirective: true`
186
- - `v-model.lazy` -> `isDirective: true`
187
-
188
- #### `v-slot` / `#`(スロット)
189
-
190
- - **パターン**: `/^(v-slot:|#)(.+)$/i`
191
- - **結果**: `isDirective: true`、`potentialName: 'v-slot:' + slotName`(raw name と異なる場合)
192
- - **例**:
193
- - `#header` -> `potentialName: 'v-slot:header'`、`isDirective: true`
194
- - `v-slot:default` -> `isDirective: true`
195
-
196
- #### その他の `v-` ディレクティブ
197
-
198
- - **パターン**: `v-` で始まる
199
- - **結果**: `isDirective: true`
200
- - **例**: `v-if`、`v-for`、`v-show`、`v-else`、`v-else-if`、`v-pre`、`v-cloak`、`v-once`、`v-memo`、`v-html`、`v-text`
201
-
202
- ## 要素タイプ検出
203
-
204
- `detectElementType()` メソッドは Vue 固有のコンポーネント検出のためのマッチャー配列を使って `super.detectElementType(nodeName, matchers)` を呼び出します:
205
-
206
- | マッチャー | 型 | マッチ対象 |
207
- | ------------------- | ------ | ----------------------------------------------- |
208
- | `'Transition'` | String | Vue 組み込み `<Transition>` コンポーネント |
209
- | `'TransitionGroup'` | String | Vue 組み込み `<TransitionGroup>` コンポーネント |
210
- | `'KeepAlive'` | String | Vue 組み込み `<KeepAlive>` コンポーネント |
211
- | `'Teleport'` | String | Vue 組み込み `<Teleport>` コンポーネント |
212
- | `'Suspense'` | String | Vue 組み込み `<Suspense>` コンポーネント |
213
- | `'component'` | String | Vue 特殊要素 `<component :is="...">` |
214
- | `'slot'` | String | Vue 特殊要素 `<slot>` |
215
- | `/^[A-Z]/` | RegExp | PascalCase のタグ名(ユーザーコンポーネント) |
216
-
217
- タグ名がこれらのいずれかにマッチする場合、`detectElementType()` は `'authored'`(コンポーネントを示す)を返します。それ以外は標準の HTML 要素検出が適用されます:
218
-
219
- - `div`、`span`、`p` 等 -> `'html'`
220
- - `x-foo`、`my-element` -> `'web-component'`
221
-
222
- `<transition>`(小文字)は組み込みリストにマッチ**しない**ため標準 HTML 要素(`'html'`)として扱われますが、`<Transition>`(PascalCase)は `'authored'` として扱われることに注意してください。
223
-
224
- ## afterFlattenNodes()
225
-
226
- `afterFlattenNodes()` メソッドは特定のオプションで基底実装を呼び出します:
227
-
228
- | オプション | 値 | 効果 |
229
- | ------------------- | ------- | -------------------------------------------------------- |
230
- | `exposeWhiteSpace` | `false` | 空白のみのテキストノードは別の無効ノードとして公開しない |
231
- | `exposeInvalidNode` | `false` | 無効なノードは公開しない |
232
- | `concatText` | `false` | 隣接するテキストノードは結合しない |
233
-
234
- これらの設定は、Vue のテンプレートパーサーが空白やノードの妥当性を生の HTML パースとは異なる方法で処理することを反映しています。
235
-
236
- ## バージョン互換性
237
-
238
- vue-eslint-parser 依存は Vue 2 と Vue 3 の両方のテンプレート構文をサポートしています。パーサーは AST レベルで Vue バージョンを区別しません -- どちらも同じ `VElement`、`VText`、`VExpressionContainer` ノードタイプを生成します。Vue 3 固有の機能(`<Teleport>` や `<Suspense>` など)はパーサーレベルの変更ではなく、要素タイプ検出を通じて処理されます。
239
-
240
- ## 制約事項
241
-
242
- ### `v-if` / `v-for` の `blockBehavior` 未対応
243
-
244
- 他のフレームワークパーサー(Svelte、Pug、Alpine、JSX、Astro)は条件分岐/ループ構文に `blockBehavior` を設定し、コアエンジンが `conditionalChildNodes()` を通じて全てのありうる子ノードパターンを列挙できるようにしています。Vue パーサーはこれを**サポートしていません**。そのため、`permitted-contents` などのルールは Vue テンプレートの `v-if`/`v-else` 分岐や `v-for` イテレーション全体のコンテンツモデル検証ができません。
245
-
246
- **実装が困難な理由:**
247
-
248
- Alpine.js では条件分岐とループに決まったパターン — `<template x-for="...">` / `<template x-if="...">` — を使用しており、`<template>` 要素をそのまま PSBlock に変換できます。Vue のディレクティブは根本的に異なる仕組みで動作します:
249
-
250
- 1. **ディレクティブは任意の要素に付与可能**: `v-if`、`v-for`、`v-else`、`v-else-if` は任意の要素に配置できます(例: `<div v-if="...">`、`<li v-for="...">`)。要素は属性検証のために有効な HTML 要素として残しつつ、コンテンツモデル解析のためにブロックとしても機能させる必要があります — この二重の役割は現在のパーサーアーキテクチャではサポートされていません。
251
-
252
- 2. **兄弟要素ベースの分岐**: `v-else` と `v-else-if` はラッパーブロックの子構造ではなく、**兄弟**要素の属性です。条件グループの構築には、現在のノード単位の `nodeize()` モデルを超えた兄弟間解析が必要です。
253
-
254
- 現在の Vue パーサーはこれらのディレクティブを属性レベルでのみ処理しており(`isDirective: true`)、属性検証エラーは抑制されますが、構造的なブロック情報はコアエンジンに提供されません。
255
-
256
- ## 主要ソースファイル
257
-
258
- | ファイル | 用途 |
259
- | -------------------------- | ------------------------------------------------------------------------------------------------------------- |
260
- | `src/parser.ts` | 全オーバーライドメソッドを持つ VueParser クラス |
261
- | `src/vue-parser/index.ts` | vue-eslint-parser ラッパーと型定義(ASTNode、ASTComment) |
262
- | `src/index.ts` | モジュールエントリーポイント、parser インスタンスを再エクスポート |
263
- | `src/index.spec.ts` | パース、ディレクティブ、名前空間をカバーする統合テスト |
264
- | `src/component-scanner.ts` | `@markuplint/pretenders` 自動スキャン用コンポーネントスキャナー(サブパスエクスポート `./component-scanner`) |
265
-
266
- ## 外部依存
267
-
268
- | 依存パッケージ | 用途 |
269
- | -------------------------- | ---------------------------------------------------------------- |
270
- | `@markuplint/ml-ast` | AST 型定義(`MLASTParentNode`、`MLASTNodeTreeItem` 等) |
271
- | `@markuplint/parser-utils` | 抽象 `Parser` クラス、`ParserError`、`Token`、`ChildToken` |
272
- | `@markuplint/html-parser` | ピア依存(直接インポートされないが、パーサーエコシステムの一部) |
273
- | `vue-eslint-parser` | Vue SFC テンプレートパース(`parse`、AST 型) |
274
-
275
- ## 統合ポイント
276
-
277
- ```mermaid
278
- flowchart TD
279
- subgraph upstream ["上流"]
280
- mlAst["@markuplint/ml-ast\n(AST 型定義)"]
281
- parserUtils["@markuplint/parser-utils\n(Parser 基底クラス)"]
282
- vueEslintParser["vue-eslint-parser\n(Vue SFC トークナイザ)"]
283
- end
284
-
285
- subgraph pkg ["@markuplint/vue-parser"]
286
- vueParser["VueParser"]
287
- compScanner["componentScanner\n(./component-scanner)"]
288
- end
289
-
290
- subgraph downstream ["下流"]
291
- mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
292
- pretenders["@markuplint/pretenders\n(自動スキャン)"]
293
- end
294
-
295
- upstream -->|"型、パース"| vueParser
296
- vueParser -->|"MLASTDocument を生成"| mlCore
297
- vueParser -->|"parse()"| compScanner
298
- compScanner -->|"ComponentScanResult"| pretenders
299
- ```
300
-
301
- ### 上流
302
-
303
- - **`@markuplint/ml-ast`** -- パーサー全体で使用される AST 型定義
304
- - **`@markuplint/parser-utils`** -- `VueParser` が拡張する抽象 `Parser` クラスと `ParserError` およびユーティリティ型
305
- - **`vue-eslint-parser`** -- テンプレートのトークン化とツリー構築を行う基盤 Vue SFC パーサー
306
-
307
- ### 下流
308
-
309
- - **`@markuplint/ml-core`** -- `VueParser` が生成する `MLASTDocument` を消費し、ルール評価のための MLDOM を構築
310
- - **`@markuplint/pretenders`** -- `./component-scanner` を動的インポートし、ルート要素・属性・スロット情報を抽出して自動スキャンに利用
311
-
312
- ## ドキュメントマップ
313
-
314
- - [メンテナンスガイド](docs/maintenance.ja.md) -- コマンド、レシピ、トラブルシューティング