@markuplint/vue-parser 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/lib/component-scanner.d.ts +6 -0
- package/lib/component-scanner.js +26 -0
- package/lib/parser.d.ts +54 -0
- package/lib/parser.js +96 -6
- package/package.json +10 -9
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
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.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @markuplint/vue-parser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [5.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* make published packages loadable from require() and put types first ([723106f](https://github.com/markuplint/markuplint/commit/723106f51aac4e60dc02db2c1a894617368b0253)), closes [#4198](https://github.com/markuplint/markuplint/issues/4198)
|
|
19
|
+
* **vue-parser:** do not report a root that is neither a component nor an element ([325825f](https://github.com/markuplint/markuplint/commit/325825ffa5a3e7d23f4b38fc1d5d80cf9fbeeacc))
|
|
20
|
+
* **vue-parser:** take every tag that Vue resolves to a component for one ([0e79724](https://github.com/markuplint/markuplint/commit/0e79724b292344c0cf2dc0ca82d173d0e0add462))
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **parser-utils:** let a parser say how an attribute name gets into the DOM ([abd81e5](https://github.com/markuplint/markuplint/commit/abd81e5d20f4ffeb0ba878e77fd4122c1a803a9f))
|
|
25
|
+
* **vue-parser:** report the lang of the script setup block from the component scanner ([45a8273](https://github.com/markuplint/markuplint/commit/45a827388329cc4beb8c245e23c500d267648412))
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
# [5.2.0](https://github.com/markuplint/markuplint/compare/v5.1.1...v5.2.0) (2026-10-13)
|
|
7
29
|
|
|
8
30
|
### Bug Fixes
|
|
@@ -58,6 +58,12 @@ export interface ComponentScanSlotWrapper {
|
|
|
58
58
|
export interface ComponentScanScriptSource {
|
|
59
59
|
readonly content: string;
|
|
60
60
|
readonly offset: number;
|
|
61
|
+
/**
|
|
62
|
+
* The `lang` attribute of the block as written (`tsx`, `ts`). The language a block is written in is
|
|
63
|
+
* the block's own declaration (https://vuejs.org/api/sfc-spec.html#pre-processors), which decides
|
|
64
|
+
* whether the script holds JSX. Absent when the block has none.
|
|
65
|
+
*/
|
|
66
|
+
readonly lang?: string;
|
|
61
67
|
}
|
|
62
68
|
/**
|
|
63
69
|
* Component scanner for Vue SFC files.
|
package/lib/component-scanner.js
CHANGED
|
@@ -5,6 +5,14 @@ function extractComponentInfo(doc) {
|
|
|
5
5
|
if (!root) {
|
|
6
6
|
return null;
|
|
7
7
|
}
|
|
8
|
+
// The parser says the root is authored, yet its name is not the one of a component
|
|
9
|
+
// (`component`, `slot` of Vue): it renders what it is given, so there is no element
|
|
10
|
+
// to pretend to be. `pretenders` would otherwise emit that name as the target, and
|
|
11
|
+
// nothing tells it from the name of an element. What a component's name is, is the
|
|
12
|
+
// parser's rule (`Parser#isComponentName`).
|
|
13
|
+
if (root.elementType === 'authored' && !parser.isComponentName(root.nodeName)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
8
16
|
const spreads = extractSpreads(root);
|
|
9
17
|
return {
|
|
10
18
|
rootElement: root.nodeName,
|
|
@@ -255,11 +263,29 @@ function extractVueScriptSetup(source) {
|
|
|
255
263
|
if (!endMatch) {
|
|
256
264
|
return null;
|
|
257
265
|
}
|
|
266
|
+
const lang = langOf(startTag);
|
|
258
267
|
return {
|
|
259
268
|
content: remaining.slice(0, endMatch.index),
|
|
260
269
|
offset: contentStart,
|
|
270
|
+
...(lang ? { lang } : {}),
|
|
261
271
|
};
|
|
262
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* The value of the `lang` attribute of a start tag. The attributes are read one by one, so that
|
|
275
|
+
* ` lang='tsx'` inside the quoted value of another attribute is not one.
|
|
276
|
+
*
|
|
277
|
+
* `@markuplint/pretenders` reads the `lang` of the other `<script>` block with the same function
|
|
278
|
+
* (`extractVueScript`), which it cannot import from here (a parser package is loaded by it
|
|
279
|
+
* dynamically, never imported), so a change to one is a change to the other.
|
|
280
|
+
*/
|
|
281
|
+
function langOf(startTag) {
|
|
282
|
+
for (const [, name, double, single, bare] of startTag.matchAll(/\s([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g)) {
|
|
283
|
+
if (name?.toLowerCase() === 'lang') {
|
|
284
|
+
return double ?? single ?? bare;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
263
289
|
/**
|
|
264
290
|
* Component scanner for Vue SFC files.
|
|
265
291
|
*
|
package/lib/parser.d.ts
CHANGED
|
@@ -47,11 +47,65 @@ declare class VueParser extends Parser<ASTNode, State> {
|
|
|
47
47
|
* > Vue SFC is a compiled format so we are able to use case-sensitive tag names in it.
|
|
48
48
|
* > We are also able to use `/>` to close a tag.
|
|
49
49
|
*
|
|
50
|
+
* A tag that is not a native one is a component to Vue, however it is written: Vue resolves
|
|
51
|
+
* the tag `T` to a component registered as `T`, `camelize(T)` or `capitalize(camelize(T))`,
|
|
52
|
+
* so `<fooBar>` and `<foo-bar>` are the component `FooBar`, and `<foobar>` is not. Here
|
|
53
|
+
* a name is a component, `authored`, when it
|
|
54
|
+
*
|
|
55
|
+
* - is a built-in component, as written (`Transition`) or in kebab-case (`transition-group`),
|
|
56
|
+
* - is a special element that renders what it is given (`component`, `slot`), which is `authored`
|
|
57
|
+
* but is no component's name for {@link isComponentName},
|
|
58
|
+
* - has a capital initial (`FooBar`), a capital letter after a lowercase letter or a digit
|
|
59
|
+
* (`fooBar`), or a `.` (`ui.Item`, a member of a namespace import of `<script setup>`).
|
|
60
|
+
*
|
|
61
|
+
* `foo-bar` is a `web-component`: the name of a custom element is also the kebab-case of a component,
|
|
62
|
+
* and markuplint does not know `compilerOptions.isCustomElement` of the project. Like the others it is
|
|
63
|
+
* matched by `FooBar` (see `MLParser.componentNameVariants`).
|
|
64
|
+
*
|
|
65
|
+
* A camelCase name of SVG (`clipPath`, `foreignObject`) is an element: Vue's `isNativeTag` takes
|
|
66
|
+
* it, and `isSVGElement` is case-sensitive.
|
|
67
|
+
*
|
|
68
|
+
* The user's `authoredElementName` replaces all of this, as it replaces any default of a parser.
|
|
69
|
+
*
|
|
50
70
|
* @see https://vuejs.org/guide/essentials/component-basics#using-a-component
|
|
51
71
|
* @see https://vuejs.org/api/built-in-components.html
|
|
52
72
|
* @see https://vuejs.org/api/built-in-special-elements.html#built-in-special-elements
|
|
73
|
+
* @see https://github.com/vuejs/core/blob/main/packages/compiler-core/src/parser.ts (`isComponent`)
|
|
53
74
|
*/
|
|
54
75
|
detectElementType(nodeName: string): import("@markuplint/ml-ast").ElementType;
|
|
76
|
+
/**
|
|
77
|
+
* Whether Vue takes a tag name for a native element: the lists of the HTML, SVG and MathML elements
|
|
78
|
+
* of `@vue/shared` that `isNativeTag` of `@vue/compiler-dom` is made of. Vue resolves any other tag to a
|
|
79
|
+
* component (`<card>` is the component `Card`), unless `compilerOptions.isCustomElement` of the project
|
|
80
|
+
* says it is a custom element, which markuplint does not know.
|
|
81
|
+
*
|
|
82
|
+
* The lists are Vue's own and the names are case-sensitive, as Vue's: an obsolete element that they
|
|
83
|
+
* do not hold (`marquee`) is no native tag to Vue, though markuplint has a specification for it.
|
|
84
|
+
*
|
|
85
|
+
* It is `MLParser#isNativeTag`, so what Vue resolves is what the document takes for native. The type
|
|
86
|
+
* selector (`Marquee` stands for `<marquee>`) reads it, and so does the guard that keeps a pretender off
|
|
87
|
+
* a native element, as `@markuplint/pretenders` does. The rules keyed on the specification still see
|
|
88
|
+
* `<marquee>`.
|
|
89
|
+
*
|
|
90
|
+
* It is not the rule of {@link isComponentName}, which tells by the form of a name alone whether it is
|
|
91
|
+
* a component's, for the target of a pretender and the root a scanner reads: `as: 'marquee'` is meant
|
|
92
|
+
* as the element there.
|
|
93
|
+
*
|
|
94
|
+
* @param name - A tag name as written, case kept
|
|
95
|
+
* @returns Whether the name is a native tag
|
|
96
|
+
*/
|
|
97
|
+
isNativeTag(name: string): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* A name that Vue takes for a component (see {@link detectElementType}) and that names no special
|
|
100
|
+
* element: a built-in component as written or in kebab-case (`Transition`, `transition-group`), a capital
|
|
101
|
+
* initial (`FooBar`), a capital letter after a lowercase letter or a digit (`fooBar`), or a `.`
|
|
102
|
+
* (`ui.Item`). `foo-bar` is not one, as it may be a custom element, nor are `component` and `slot`, which render
|
|
103
|
+
* what they are given: they have no element to pretend to be.
|
|
104
|
+
*
|
|
105
|
+
* @param name - A tag name as written, case kept
|
|
106
|
+
* @returns Whether the name is a component's
|
|
107
|
+
*/
|
|
108
|
+
isComponentName(name: string): boolean;
|
|
55
109
|
}
|
|
56
110
|
export declare const parser: VueParser;
|
|
57
111
|
export {};
|
package/lib/parser.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
|
-
import { ParserError, Parser } from '@markuplint/parser-utils';
|
|
1
|
+
import { ParserError, Parser, isPotentialCustomElementName, isSVGElement } from '@markuplint/parser-utils';
|
|
2
|
+
import { isHTMLTag, isMathMLTag, isSVGTag } from '@vue/shared';
|
|
2
3
|
import { vueParse } from './vue-parser/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* The built-in components, as written or in kebab-case: `isBuiltInType(tag, name)` of Vue is
|
|
6
|
+
* `tag === name || tag === hyphenate(name)`.
|
|
7
|
+
*
|
|
8
|
+
* @see https://vuejs.org/api/built-in-components.html
|
|
9
|
+
*/
|
|
10
|
+
const BUILT_IN_COMPONENTS = [
|
|
11
|
+
'Transition',
|
|
12
|
+
'transition',
|
|
13
|
+
'TransitionGroup',
|
|
14
|
+
'transition-group',
|
|
15
|
+
'KeepAlive',
|
|
16
|
+
'keep-alive',
|
|
17
|
+
'Teleport',
|
|
18
|
+
'teleport',
|
|
19
|
+
'Suspense',
|
|
20
|
+
'suspense',
|
|
21
|
+
];
|
|
3
22
|
/**
|
|
4
23
|
* Directive resolution is intentionally not performed by this parser:
|
|
5
24
|
* `directivePatterns` declared in `@markuplint/vue-spec` are applied later by
|
|
@@ -27,6 +46,11 @@ class VueParser extends Parser {
|
|
|
27
46
|
// Vue SFC is a compiled format that uses explicit XML-style
|
|
28
47
|
// closing tags (including `/>`), not HTML void-element rules.
|
|
29
48
|
endTagType: 'xml',
|
|
49
|
+
// Vue resolves `<FooBar>`, `<fooBar>` and `<foo-bar>` to the same component.
|
|
50
|
+
componentNameVariants: true,
|
|
51
|
+
// The compiler of Vue makes an element by `setAttribute` unless it can put the whole static tree into
|
|
52
|
+
// an HTML string, which it does only for a name it knows by its exact spelling.
|
|
53
|
+
attrNameRoute: 'dom-api',
|
|
30
54
|
}, {
|
|
31
55
|
comments: [],
|
|
32
56
|
});
|
|
@@ -154,24 +178,90 @@ class VueParser extends Parser {
|
|
|
154
178
|
* > Vue SFC is a compiled format so we are able to use case-sensitive tag names in it.
|
|
155
179
|
* > We are also able to use `/>` to close a tag.
|
|
156
180
|
*
|
|
181
|
+
* A tag that is not a native one is a component to Vue, however it is written: Vue resolves
|
|
182
|
+
* the tag `T` to a component registered as `T`, `camelize(T)` or `capitalize(camelize(T))`,
|
|
183
|
+
* so `<fooBar>` and `<foo-bar>` are the component `FooBar`, and `<foobar>` is not. Here
|
|
184
|
+
* a name is a component, `authored`, when it
|
|
185
|
+
*
|
|
186
|
+
* - is a built-in component, as written (`Transition`) or in kebab-case (`transition-group`),
|
|
187
|
+
* - is a special element that renders what it is given (`component`, `slot`), which is `authored`
|
|
188
|
+
* but is no component's name for {@link isComponentName},
|
|
189
|
+
* - has a capital initial (`FooBar`), a capital letter after a lowercase letter or a digit
|
|
190
|
+
* (`fooBar`), or a `.` (`ui.Item`, a member of a namespace import of `<script setup>`).
|
|
191
|
+
*
|
|
192
|
+
* `foo-bar` is a `web-component`: the name of a custom element is also the kebab-case of a component,
|
|
193
|
+
* and markuplint does not know `compilerOptions.isCustomElement` of the project. Like the others it is
|
|
194
|
+
* matched by `FooBar` (see `MLParser.componentNameVariants`).
|
|
195
|
+
*
|
|
196
|
+
* A camelCase name of SVG (`clipPath`, `foreignObject`) is an element: Vue's `isNativeTag` takes
|
|
197
|
+
* it, and `isSVGElement` is case-sensitive.
|
|
198
|
+
*
|
|
199
|
+
* The user's `authoredElementName` replaces all of this, as it replaces any default of a parser.
|
|
200
|
+
*
|
|
157
201
|
* @see https://vuejs.org/guide/essentials/component-basics#using-a-component
|
|
158
202
|
* @see https://vuejs.org/api/built-in-components.html
|
|
159
203
|
* @see https://vuejs.org/api/built-in-special-elements.html#built-in-special-elements
|
|
204
|
+
* @see https://github.com/vuejs/core/blob/main/packages/compiler-core/src/parser.ts (`isComponent`)
|
|
160
205
|
*/
|
|
161
206
|
detectElementType(nodeName) {
|
|
162
207
|
return super.detectElementType(nodeName, [
|
|
163
208
|
// Built-in components
|
|
164
|
-
|
|
165
|
-
'TransitionGroup',
|
|
166
|
-
'KeepAlive',
|
|
167
|
-
'Teleport',
|
|
168
|
-
'Suspense',
|
|
209
|
+
...BUILT_IN_COMPONENTS,
|
|
169
210
|
// Special elements
|
|
170
211
|
'component',
|
|
171
212
|
'slot',
|
|
172
213
|
// Backward compatibility
|
|
173
214
|
/^[A-Z]/,
|
|
215
|
+
// A member of a namespace import
|
|
216
|
+
/\./,
|
|
217
|
+
// camelCase
|
|
218
|
+
name => /[a-z\d][A-Z]/.test(name) && !isSVGElement(name),
|
|
174
219
|
]);
|
|
175
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Whether Vue takes a tag name for a native element: the lists of the HTML, SVG and MathML elements
|
|
223
|
+
* of `@vue/shared` that `isNativeTag` of `@vue/compiler-dom` is made of. Vue resolves any other tag to a
|
|
224
|
+
* component (`<card>` is the component `Card`), unless `compilerOptions.isCustomElement` of the project
|
|
225
|
+
* says it is a custom element, which markuplint does not know.
|
|
226
|
+
*
|
|
227
|
+
* The lists are Vue's own and the names are case-sensitive, as Vue's: an obsolete element that they
|
|
228
|
+
* do not hold (`marquee`) is no native tag to Vue, though markuplint has a specification for it.
|
|
229
|
+
*
|
|
230
|
+
* It is `MLParser#isNativeTag`, so what Vue resolves is what the document takes for native. The type
|
|
231
|
+
* selector (`Marquee` stands for `<marquee>`) reads it, and so does the guard that keeps a pretender off
|
|
232
|
+
* a native element, as `@markuplint/pretenders` does. The rules keyed on the specification still see
|
|
233
|
+
* `<marquee>`.
|
|
234
|
+
*
|
|
235
|
+
* It is not the rule of {@link isComponentName}, which tells by the form of a name alone whether it is
|
|
236
|
+
* a component's, for the target of a pretender and the root a scanner reads: `as: 'marquee'` is meant
|
|
237
|
+
* as the element there.
|
|
238
|
+
*
|
|
239
|
+
* @param name - A tag name as written, case kept
|
|
240
|
+
* @returns Whether the name is a native tag
|
|
241
|
+
*/
|
|
242
|
+
isNativeTag(name) {
|
|
243
|
+
return isHTMLTag(name) || isSVGTag(name) || isMathMLTag(name);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* A name that Vue takes for a component (see {@link detectElementType}) and that names no special
|
|
247
|
+
* element: a built-in component as written or in kebab-case (`Transition`, `transition-group`), a capital
|
|
248
|
+
* initial (`FooBar`), a capital letter after a lowercase letter or a digit (`fooBar`), or a `.`
|
|
249
|
+
* (`ui.Item`). `foo-bar` is not one, as it may be a custom element, nor are `component` and `slot`, which render
|
|
250
|
+
* what they are given: they have no element to pretend to be.
|
|
251
|
+
*
|
|
252
|
+
* @param name - A tag name as written, case kept
|
|
253
|
+
* @returns Whether the name is a component's
|
|
254
|
+
*/
|
|
255
|
+
isComponentName(name) {
|
|
256
|
+
// `<transition>` is `<Transition>`, and a tag with a `.` is resolved by the name before it (`my-widget.v2` by
|
|
257
|
+
// `my-widget`): a component by Vue, whatever a custom element name would say of it
|
|
258
|
+
if (BUILT_IN_COMPONENTS.includes(name) || name.includes('.')) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (isPotentialCustomElementName(name)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return /^[A-Z]/.test(name) || (/[a-z\d][A-Z]/.test(name) && !isSVGElement(name));
|
|
265
|
+
}
|
|
176
266
|
}
|
|
177
267
|
export const parser = new VueParser();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/vue-parser",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "Vue parser for markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
21
|
"./component-scanner": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"types": "./lib/component-scanner.d.ts",
|
|
23
|
+
"default": "./lib/component-scanner.js"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@markuplint/html-parser": "5.
|
|
36
|
-
"@markuplint/ml-ast": "5.
|
|
37
|
-
"@markuplint/parser-utils": "5.
|
|
35
|
+
"@markuplint/html-parser": "5.4.0",
|
|
36
|
+
"@markuplint/ml-ast": "5.3.0",
|
|
37
|
+
"@markuplint/parser-utils": "5.4.0",
|
|
38
|
+
"@vue/shared": "^3.5.44",
|
|
38
39
|
"vue-eslint-parser": "^10.4.1"
|
|
39
40
|
},
|
|
40
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "647f6fb5d5743f060678599c97e9c84b58970747"
|
|
41
42
|
}
|