@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/ARCHITECTURE.md DELETED
@@ -1,314 +0,0 @@
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
- ├── component-scanner.ts — Component scanner for pretenders auto scan (subpath export)
14
- ├── index.spec.ts — Integration tests for VueParser
15
- ├── component-scanner.spec.ts — Tests for component scanner
16
- └── vue-parser/
17
- └── index.ts — vue-eslint-parser wrapper, ASTNode/ASTComment type exports
18
- ```
19
-
20
- ## Architecture Diagram
21
-
22
- ```mermaid
23
- flowchart TD
24
- subgraph upstream ["Upstream"]
25
- mlAst["@markuplint/ml-ast\n(AST types)"]
26
- parserUtils["@markuplint/parser-utils\n(Abstract Parser class)"]
27
- vueEslintParser["vue-eslint-parser\n(Vue SFC tokenizer)"]
28
- end
29
-
30
- subgraph pkg ["@markuplint/vue-parser"]
31
- vueParser["VueParser\nextends Parser‹ASTNode, State›"]
32
- vueParseFn["vueParse()\nvue-eslint-parser wrapper"]
33
- compScanner["componentScanner\n(subpath: ./component-scanner)"]
34
- end
35
-
36
- subgraph downstream ["Downstream"]
37
- mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
38
- pretenders["@markuplint/pretenders\n(auto scan)"]
39
- end
40
-
41
- mlAst -->|"AST types"| vueParser
42
- parserUtils -->|"Parser base class"| vueParser
43
- vueEslintParser -->|"parse()"| vueParseFn
44
- vueParseFn -->|"ESLintProgram AST"| vueParser
45
-
46
- vueParser -->|"produces MLASTDocument"| mlCore
47
- vueParser -->|"parse()"| compScanner
48
- compScanner -->|"ComponentScanResult"| pretenders
49
- ```
50
-
51
- ## VueParser Class
52
-
53
- ### Inheritance
54
-
55
- ```
56
- Parser<ASTNode, State> (from @markuplint/parser-utils)
57
- └── VueParser (this package)
58
- ```
59
-
60
- ### Constructor
61
-
62
- The constructor configures the parser with two arguments:
63
-
64
- | Argument | Value | Purpose |
65
- | --------------- | ----------------------- | ------------------------------------------------------------------------ |
66
- | `ParserOptions` | `{ endTagType: 'xml' }` | Vue templates use explicit closing tags (XML-style), not HTML void rules |
67
- | Initial State | `{ comments: [] }` | Empty comments array, populated during `tokenize()` |
68
-
69
- The `tagNameCaseSensitive` behavior is inherited from the base class and combined with Vue's `detectElementType` override to correctly handle PascalCase component names.
70
-
71
- ### State Type
72
-
73
- The parser maintains internal state through the `State` type:
74
-
75
- | Field | Type | Purpose |
76
- | ---------- | ----------------------- | -------------------------------------------------------------------------------------------------- |
77
- | `comments` | `readonly ASTComment[]` | Template comments extracted from vue-eslint-parser during tokenize, injected later in flattenNodes |
78
-
79
- ### Override Methods
80
-
81
- | Method | Purpose |
82
- | --------------------- | ---------------------------------------------------------------------------------------------- |
83
- | `tokenize()` | Invokes vue-eslint-parser and extracts `templateBody.children` and comments |
84
- | `parseError()` | Converts vue-eslint-parser `SyntaxError` (with `lineNumber`/`column`) into `ParserError` |
85
- | `nodeize()` | Converts vue-eslint-parser AST nodes (VText, VElement, VExpressionContainer) to markuplint AST |
86
- | `flattenNodes()` | Extends base flattening to inject template comments between sibling nodes |
87
- | `afterFlattenNodes()` | Calls base with `exposeWhiteSpace: false`, `exposeInvalidNode: false`, `concatText: false` |
88
- | `detectElementType()` | Detects PascalCase components and Vue built-in components |
89
-
90
- > **Note:** The `visitAttr()` override has been removed. Vue directive handling (`v-bind`, `v-on`, `v-model`, `v-slot`, etc.) is now managed via `directivePatterns` in `@markuplint/vue-spec`.
91
-
92
- ### `duplicatableAttrs`
93
-
94
- A `Set<string>` containing `'class'` and `'style'` -- attributes that may appear multiple times on a single element (via `v-bind:class` alongside `class`).
95
-
96
- ## tokenize()
97
-
98
- The `tokenize()` method is the entry point for obtaining the vue-eslint-parser AST:
99
-
100
- 1. Calls `vueParse(this.rawCode)` which invokes `VueESLintParser.parse(vueTemplate, { parser: false })`
101
- 2. If `ast.templateBody?.comments` exists, stores them in `this.state.comments` for later injection
102
- 3. Returns `{ ast: ast.templateBody?.children ?? [], isFragment: true }`
103
-
104
- 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.
105
-
106
- ## nodeize() Details
107
-
108
- The `nodeize()` method dispatches based on the `originNode.type` field:
109
-
110
- ### VText -> visitText
111
-
112
- 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.
113
-
114
- ### VExpressionContainer -> visitPsBlock
115
-
116
- Expression containers like `{{ expression }}` are converted to pseudo-block nodes via `visitPsBlock()`:
117
-
118
- - `nodeName`: `'vue-expression-container'`
119
- - `isFragment`: `false`
120
-
121
- This treats Vue template expressions as opaque blocks in the markuplint AST rather than attempting to parse their JavaScript content.
122
-
123
- ### VElement -> visitElement
124
-
125
- For element nodes, the method:
126
-
127
- 1. Slices the **start tag** token from `originNode.startTag.range`
128
- 2. Calls `visitElement()` with the element's `name` and `namespace`
129
- 3. Passes `originNode.children` as child nodes -- these are `templateBody.children` when the node represents the template root
130
- 4. Creates an end tag token factory (`createEndTagToken`) that returns `null` if the element is self-closing, otherwise slices from `originNode.endTag.range`
131
-
132
- ## flattenNodes()
133
-
134
- The `flattenNodes()` method extends the base `Parser.flattenNodes()` to inject template comments:
135
-
136
- 1. Calls `super.flattenNodes(nodeTree)` to get the initial flat node list
137
- 2. Iterates through the node list, checking for comments between each pair of adjacent nodes
138
- 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
139
- 4. When a comment is found, creates it via `this.visitComment()` with `isBogus` set based on the comment's type (`HTMLBogusComment` vs standard)
140
- 5. Appends the comment to the parent node via `this.appendChild()`
141
-
142
- 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.
143
-
144
- ## Directive Handling (directivePatterns in @markuplint/vue-spec)
145
-
146
- Vue directive resolution is managed by `directivePatterns` defined in `@markuplint/vue-spec`, not by the parser itself. The spec declares patterns that the core engine uses to map directives to `potentialName`, `isDirective`, and `isDynamicValue` metadata.
147
-
148
- > **Two-stage resolution:** Parser-level tests (`index.spec.ts`) show raw AST values where `isDynamicValue` and `isDirective` reflect only what the parser itself sets (e.g., curly-brace expressions). Core-level tests (`ml-core` and `rules`) show the final resolved values after `directivePatterns` are applied by `ml-core`'s `MLAttr` constructor. For example, `on:click` without a value shows `isDynamicValue: false` at the parser level, but resolves to `isDynamicValue: true` at the core level via the `directivePatterns` match.
149
-
150
- ### Quote Set
151
-
152
- 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.
153
-
154
- ### Vue Directive Processing
155
-
156
- Directives are processed in priority order. The first matching pattern wins:
157
-
158
- #### `v-on` / `@` (Event Binding)
159
-
160
- - **Pattern**: `/^(v-on:|@)([^.]+)(?:\.([^.]+))?$/i`
161
- - **Result**: `potentialName: 'on' + eventName.toLowerCase()`, `isDynamicValue: true`
162
- - **Examples**:
163
- - `@click` -> `potentialName: 'onclick'`
164
- - `v-on:click.stop` -> `potentialName: 'onclick'`
165
- - `@keydown.enter` -> `potentialName: 'onkeydown'`
166
-
167
- #### `v-bind` / `:` (Property Binding)
168
-
169
- - **Pattern**: `/^(v-bind:|:)([^.]+)(?:\.([^.]+))?$/i`
170
- - **Result** (no modifier): `potentialName: propName`, `isDynamicValue: true`
171
- - **Result** (`.attr` modifier): `potentialName: propName`, `isDynamicValue: true`
172
- - **Result** (`.prop` / `.camel` / other modifiers): `isDirective: true`, `potentialName` set to normalized form
173
- - **`isDuplicatable`**: If the bound property is in `duplicatableAttrs` (class, style), `isDuplicatable` is set to `true`
174
- - **Examples**:
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
- - **Pattern**: `/^(v-model)(?:\.([^.]+))?$/i`
183
- - **Result**: `isDirective: true`
184
- - **Examples**:
185
- - `v-model` -> `isDirective: true`
186
- - `v-model.lazy` -> `isDirective: true`
187
-
188
- #### `v-slot` / `#` (Slot)
189
-
190
- - **Pattern**: `/^(v-slot:|#)(.+)$/i`
191
- - **Result**: `isDirective: true`, `potentialName: 'v-slot:' + slotName` (if different from raw name)
192
- - **Examples**:
193
- - `#header` -> `potentialName: 'v-slot:header'`, `isDirective: true`
194
- - `v-slot:default` -> `isDirective: true`
195
-
196
- #### Other `v-` Directives
197
-
198
- - **Pattern**: Starts with `v-`
199
- - **Result**: `isDirective: true`
200
- - **Examples**: `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
- ## Element Type Detection
203
-
204
- The `detectElementType()` method calls `super.detectElementType(nodeName, matchers)` with an array of matchers for Vue-specific component detection:
205
-
206
- | Matcher | Type | Matches |
207
- | ------------------- | ------ | ------------------------------------------- |
208
- | `'Transition'` | String | Vue built-in `<Transition>` component |
209
- | `'TransitionGroup'` | String | Vue built-in `<TransitionGroup>` component |
210
- | `'KeepAlive'` | String | Vue built-in `<KeepAlive>` component |
211
- | `'Teleport'` | String | Vue built-in `<Teleport>` component |
212
- | `'Suspense'` | String | Vue built-in `<Suspense>` component |
213
- | `'component'` | String | Vue special element `<component :is="...">` |
214
- | `'slot'` | String | Vue special element `<slot>` |
215
- | `/^[A-Z]/` | RegExp | Any PascalCase tag name (user components) |
216
-
217
- When a tag name matches any of these, `detectElementType()` returns `'authored'` (indicating a component). Otherwise, standard HTML element detection applies:
218
-
219
- - `div`, `span`, `p` etc. -> `'html'`
220
- - `x-foo`, `my-element` -> `'web-component'`
221
-
222
- 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'`.
223
-
224
- ## afterFlattenNodes()
225
-
226
- The `afterFlattenNodes()` method calls the base implementation with specific options:
227
-
228
- | Option | Value | Effect |
229
- | ------------------- | ------- | -------------------------------------------------------------------- |
230
- | `exposeWhiteSpace` | `false` | Whitespace-only text nodes are not exposed as separate invalid nodes |
231
- | `exposeInvalidNode` | `false` | Invalid nodes are not exposed |
232
- | `concatText` | `false` | Adjacent text nodes are not concatenated |
233
-
234
- These settings reflect that Vue's template parser handles whitespace and node validity differently from raw HTML parsing.
235
-
236
- ## Version Compatibility
237
-
238
- 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.
239
-
240
- ## Limitations
241
-
242
- ### No `blockBehavior` support for `v-if` / `v-for`
243
-
244
- Other framework parsers (Svelte, Pug, Alpine, JSX, Astro) set `blockBehavior` on their conditional/loop constructs so that the core engine can enumerate all possible child node patterns via `conditionalChildNodes()`. The Vue parser does **not** support this. As a result, rules like `permitted-contents` cannot validate content models across `v-if`/`v-else` branches or `v-for` iterations in Vue templates.
245
-
246
- **Why this is difficult to implement:**
247
-
248
- In Alpine.js, conditionals and loops use a fixed pattern — `<template x-for="...">` / `<template x-if="...">` — where the `<template>` element can be cleanly converted into a PSBlock. Vue's directives work fundamentally differently:
249
-
250
- 1. **Directives attach to arbitrary elements**: `v-if`, `v-for`, `v-else`, and `v-else-if` can appear on any element (e.g., `<div v-if="...">`, `<li v-for="...">`). The element must remain a valid HTML element for attribute validation while simultaneously acting as a block for content model analysis — a dual role the current parser architecture does not support.
251
-
252
- 2. **Sibling-based branching**: `v-else` and `v-else-if` are attributes on **sibling** elements, not child constructs of a wrapper block. Building conditional groups requires cross-sibling analysis that goes beyond the current per-node `nodeize()` model.
253
-
254
- The current Vue parser handles these directives at the attribute level only (`isDirective: true`), which suppresses attribute validation errors but does not provide structural block information to the core engine.
255
-
256
- ## Key Source Files
257
-
258
- | File | Purpose |
259
- | -------------------------- | ----------------------------------------------------------------------------------------------- |
260
- | `src/parser.ts` | VueParser class with all override methods |
261
- | `src/vue-parser/index.ts` | vue-eslint-parser wrapper and type definitions (ASTNode, ASTComment) |
262
- | `src/index.ts` | Module entry point, re-exports parser instance |
263
- | `src/index.spec.ts` | Integration tests covering parsing, directives, namespaces |
264
- | `src/component-scanner.ts` | Component scanner for `@markuplint/pretenders` auto scan (subpath export `./component-scanner`) |
265
-
266
- ## External Dependencies
267
-
268
- | Dependency | Purpose |
269
- | -------------------------- | -------------------------------------------------------------------- |
270
- | `@markuplint/ml-ast` | AST type definitions (`MLASTParentNode`, `MLASTNodeTreeItem`, etc.) |
271
- | `@markuplint/parser-utils` | Abstract `Parser` class, `ParserError`, `Token`, `ChildToken` |
272
- | `@markuplint/html-parser` | Peer dependency (not directly imported but part of parser ecosystem) |
273
- | `vue-eslint-parser` | Vue SFC template parsing (`parse`, AST types) |
274
-
275
- ## Integration Points
276
-
277
- ```mermaid
278
- flowchart TD
279
- subgraph upstream ["Upstream"]
280
- mlAst["@markuplint/ml-ast\n(AST types)"]
281
- parserUtils["@markuplint/parser-utils\n(Parser base class)"]
282
- vueEslintParser["vue-eslint-parser\n(Vue SFC tokenizer)"]
283
- end
284
-
285
- subgraph pkg ["@markuplint/vue-parser"]
286
- vueParser["VueParser"]
287
- compScanner["componentScanner\n(./component-scanner)"]
288
- end
289
-
290
- subgraph downstream ["Downstream"]
291
- mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
292
- pretenders["@markuplint/pretenders\n(auto scan)"]
293
- end
294
-
295
- upstream -->|"types, parsing"| vueParser
296
- vueParser -->|"produces MLASTDocument"| mlCore
297
- vueParser -->|"parse()"| compScanner
298
- compScanner -->|"ComponentScanResult"| pretenders
299
- ```
300
-
301
- ### Upstream
302
-
303
- - **`@markuplint/ml-ast`** -- AST type definitions used throughout the parser
304
- - **`@markuplint/parser-utils`** -- Abstract `Parser` class that `VueParser` extends, plus `ParserError` and utility types
305
- - **`vue-eslint-parser`** -- The underlying Vue SFC parser that performs template tokenization and tree construction
306
-
307
- ### Downstream
308
-
309
- - **`@markuplint/ml-core`** -- Consumes the `MLASTDocument` produced by `VueParser` and constructs the MLDOM for rule evaluation
310
- - **`@markuplint/pretenders`** -- Dynamically imports `./component-scanner` to extract root element, attributes, and slot information for auto scan
311
-
312
- ## Documentation Map
313
-
314
- - [Maintenance Guide](docs/maintenance.md) -- Commands, recipes, and troubleshooting
package/SKILL.md DELETED
@@ -1,161 +0,0 @@
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 directivePatterns (vue-spec) |
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
- | `update-component-scanner` | Update component-scanner for pretenders auto scan |
24
-
25
- If omitted, defaults to `add-directive`.
26
-
27
- ## Reference
28
-
29
- Before executing any task, read `docs/maintenance.md` (or `docs/maintenance.ja.md`)
30
- for the full guide. The recipes there are the source of truth for procedures.
31
-
32
- Also read:
33
-
34
- - `ARCHITECTURE.md` -- Package overview, directive processing, and element type detection
35
- - `src/parser.ts` -- VueParser class (source of truth for all override methods)
36
-
37
- ## Task: add-directive
38
-
39
- Add or modify a Vue directive in `directivePatterns` (`@markuplint/vue-spec`). Follow recipe #1 in `docs/maintenance.md`.
40
-
41
- ### Step 1: Understand the directive
42
-
43
- 1. Open `@markuplint/vue-spec` (`packages/@markuplint/vue-spec/src/index.ts`) and find the `directivePatterns` array
44
- 2. Identify where in the priority chain the new directive should be processed
45
- 3. Determine whether the directive needs `potentialName`, `isDirective`, or `isDynamicValue`
46
-
47
- ### Step 2: Add the directive pattern
48
-
49
- 1. Create a new regex block following the existing pattern (scoped block with destructuring)
50
- 2. Use `attr.name.raw.match()` to extract the directive prefix and value
51
- 3. Set the appropriate metadata:
52
- - `potentialName` — maps the directive to an equivalent HTML attribute name
53
- - `isDirective` — marks as a Vue-only directive with no HTML equivalent
54
- - `isDynamicValue` — indicates the attribute value is a JavaScript expression
55
- 4. Check if the attribute should be in `duplicatableAttrs` (e.g., class, style)
56
-
57
- ### Step 3: Verify
58
-
59
- 1. Build: `yarn build --scope @markuplint/vue-parser`
60
- 2. Add test cases to `src/index.spec.ts` covering:
61
- - The directive with its full form (e.g., `v-bind:prop`)
62
- - The shorthand form if applicable (e.g., `:prop`)
63
- - Edge cases with modifiers (e.g., `.stop`, `.lazy`)
64
- 3. Test: `yarn test --scope @markuplint/vue-parser`
65
-
66
- ## Task: modify-element-type-detection
67
-
68
- Update `detectElementType()` matchers. Follow recipe #2 in `docs/maintenance.md`.
69
-
70
- ### Step 1: Understand current matchers
71
-
72
- 1. Read the `detectElementType()` method in `src/parser.ts`
73
- 2. Review the matcher array: string literals for built-in components, regex for PascalCase
74
-
75
- ### Step 2: Make the change
76
-
77
- 1. To add a new built-in component: add the component name as a string to the matcher array
78
- 2. To add a new pattern: add a regex to the matcher array
79
- 3. Components matching any entry return `'authored'` element type
80
-
81
- ### Step 3: Verify
82
-
83
- 1. Build: `yarn build --scope @markuplint/vue-parser`
84
- 2. Add test cases to the `elementType` test block in `src/index.spec.ts`
85
- 3. Test: `yarn test --scope @markuplint/vue-parser`
86
-
87
- ## Task: update-vue-version-support
88
-
89
- Handle vue-eslint-parser API changes. Follow recipe #3 in `docs/maintenance.md`.
90
-
91
- ### Step 1: Understand the change
92
-
93
- 1. Read `src/vue-parser/index.ts` — the wrapper around vue-eslint-parser
94
- 2. Check the vue-eslint-parser changelog for breaking changes
95
- 3. Verify the `ASTNode` and `ASTComment` type exports still match
96
-
97
- ### Step 2: Make the change
98
-
99
- 1. Update `vueParse()` if the `parse()` API signature changed
100
- 2. Update type exports (`ASTNode`, `ASTComment`, `VueTokens`) if AST types changed
101
- 3. Update `tokenize()` in `src/parser.ts` if `templateBody` structure changed
102
-
103
- ### Step 3: Verify
104
-
105
- 1. Build: `yarn build --scope @markuplint/vue-parser`
106
- 2. Test: `yarn test --scope @markuplint/vue-parser`
107
- 3. Test with real Vue SFC files to ensure correct parsing
108
-
109
- ## Task: fix-comment-injection
110
-
111
- Fix template comment injection in `flattenNodes()`. Follow recipe #4 in `docs/maintenance.md`.
112
-
113
- ### Step 1: Understand the issue
114
-
115
- 1. Read the `flattenNodes()` method in `src/parser.ts`
116
- 2. Understand the two-pass approach: first flatten, then inject comments
117
- 3. Check how `this.state.comments` is populated in `tokenize()`
118
-
119
- ### Step 2: Fix the injection logic
120
-
121
- 1. Verify the comment range check: `lastOffset <= comment.range[0] && comment.range[1] <= node.startOffset`
122
- 2. Verify `this.visitComment()` is called with the correct `isBogus` flag
123
- 3. Verify `this.appendChild()` correctly attaches the comment to the parent
124
-
125
- ### Step 3: Verify
126
-
127
- 1. Build: `yarn build --scope @markuplint/vue-parser`
128
- 2. Test with Vue templates containing HTML comments (`<!-- -->`), bogus comments (`<!...>`), and mixed content
129
- 3. Test: `yarn test --scope @markuplint/vue-parser`
130
-
131
- ## Task: update-component-scanner
132
-
133
- Update `src/component-scanner.ts` when Vue slot syntax or script block handling changes.
134
-
135
- ### When to update
136
-
137
- - New slot-like syntax is added to Vue (e.g., a new built-in component that acts as a slot)
138
- - `<script setup>` extraction logic needs to change (e.g., new script attributes)
139
- - The `extractComponentInfo` shared logic needs a fix (also update svelte-parser and astro-parser)
140
-
141
- ### Step 1: Make the change
142
-
143
- 1. Read `src/component-scanner.ts`
144
- 2. Modify `detectSlots()` for new slot patterns, or `extractVueScriptSetup()` for script changes
145
- 3. If modifying `extractComponentInfo()`, apply the same change to all three parsers (vue, svelte, astro)
146
-
147
- ### Step 2: Verify
148
-
149
- 1. Update tests in `src/component-scanner.spec.ts`
150
- 2. Build: `yarn build --scope @markuplint/vue-parser`
151
- 3. Test: `npx vitest run packages/@markuplint/vue-parser/src/component-scanner.spec.ts`
152
- 4. Run pretenders integration tests: `npx vitest run packages/@markuplint/pretenders`
153
-
154
- ## Rules
155
-
156
- 1. **Use vue-eslint-parser** for all template parsing — never parse Vue templates manually.
157
- 2. **Use `potentialName`** for directives that map to HTML attributes (e.g., `@click` -> `onclick`).
158
- 3. **Use `isDirective: true`** for directives with no HTML equivalent (e.g., `v-if`, `v-for`).
159
- 4. **Test with `nodeListToDebugMaps`** — this is the standard assertion pattern for parser tests.
160
- 5. **Add JSDoc comments** to all new public methods and properties.
161
- 6. **Preserve directive priority order** in `directivePatterns` — `.prop` shorthand before `v-bind` with modifiers before `v-bind`/`:` before `v-on`/`@` before `v-model` before `v-slot`/`#` before generic `v-`.