@markuplint/parser-utils 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 +31 -0
- package/lib/debug.d.ts +4 -0
- package/lib/debug.js +7 -0
- package/lib/decision.d.ts +25 -0
- package/lib/decision.js +27 -0
- package/lib/detect-element-type.js +14 -3
- package/lib/idl-attributes.d.ts +18 -3
- package/lib/idl-attributes.js +45 -11
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/parser.d.ts +63 -3
- package/lib/parser.js +80 -3
- package/lib/types.d.ts +4 -1
- package/package.json +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,37 @@
|
|
|
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/parser-utils
|
|
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
|
+
* **parser-utils:** map an IDL name by its exact spelling ([3eac033](https://github.com/markuplint/markuplint/commit/3eac033da5d20884bf00f4defeccb3be9c7d44bf))
|
|
20
|
+
* **rules:** tell the namespace of a component's prop by the name as written ([157f434](https://github.com/markuplint/markuplint/commit/157f434b9dc4bb0f51c3fb77b219f38ef4035dd8))
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **ml-core:** ask whether a name is a prefix of an attribute, and keep suggesting the property ([bd195ef](https://github.com/markuplint/markuplint/commit/bd195ef6ededb80482c642a86360c5f932f8b91b))
|
|
25
|
+
* **parser-utils:** add isComponentName to tell a component name from an element name ([3385e5b](https://github.com/markuplint/markuplint/commit/3385e5b874290ec6c9d23c769d50e8741a0ce8af))
|
|
26
|
+
* **parser-utils:** add the attrNameCaseSensitive parser option ([d494501](https://github.com/markuplint/markuplint/commit/d49450161f5de6dfcd8e035db5b39eeb3815d396)), closes [#4229](https://github.com/markuplint/markuplint/issues/4229)
|
|
27
|
+
* **parser-utils:** let a parser decide which character starts a tag name ([720022f](https://github.com/markuplint/markuplint/commit/720022ff33bd103c20827e232acb16a0da8230bc))
|
|
28
|
+
* **parser-utils:** let a parser own the rule of its component names ([92d5048](https://github.com/markuplint/markuplint/commit/92d5048a174bfa39e0624ece4ec71e0b00a2bae0)), closes [Parser#isComponentName](https://github.com/Parser/issues/isComponentName)
|
|
29
|
+
* **parser-utils:** let a parser say how an attribute name gets into the DOM ([abd81e5](https://github.com/markuplint/markuplint/commit/abd81e5d20f4ffeb0ba878e77fd4122c1a803a9f))
|
|
30
|
+
|
|
31
|
+
### Performance Improvements
|
|
32
|
+
|
|
33
|
+
* **parser-utils:** skip debug maps when logging is disabled ([47466b0](https://github.com/markuplint/markuplint/commit/47466b00d5d4c4fc121ed55d22e11540cd1ed0cd))
|
|
34
|
+
* **selector:** read the debug flag once per match and pass it down ([1e99fc0](https://github.com/markuplint/markuplint/commit/1e99fc095aa57c9cadf08a55e109a1f75c0b31ca))
|
|
35
|
+
|
|
36
|
+
|
|
6
37
|
# [5.2.0](https://github.com/markuplint/markuplint/compare/v5.1.1...v5.2.0) (2026-10-13)
|
|
7
38
|
|
|
8
39
|
### Bug Fixes
|
package/lib/debug.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { MLASTNode } from '@markuplint/ml-ast';
|
|
2
2
|
import debug from 'debug';
|
|
3
|
+
/**
|
|
4
|
+
* Read `.enabled` at call time; never cache it at module load. `debug.enable()` (including via `verbosely()` /
|
|
5
|
+
* `enableDebug()`) can switch a namespace on after import.
|
|
6
|
+
*/
|
|
3
7
|
export declare const log: debug.Debugger;
|
|
4
8
|
export declare function domLog(nodeList: readonly (MLASTNode | null)[]): void;
|
|
5
9
|
export declare class PerformanceTimer {
|
package/lib/debug.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
2
|
import { nodeListToDebugMaps } from './debugger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Read `.enabled` at call time; never cache it at module load. `debug.enable()` (including via `verbosely()` /
|
|
5
|
+
* `enableDebug()`) can switch a namespace on after import.
|
|
6
|
+
*/
|
|
3
7
|
export const log = debug('ml-parser');
|
|
4
8
|
export function domLog(nodeList) {
|
|
9
|
+
if (!log.enabled) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
5
12
|
log('Parse result: %O', nodeListToDebugMaps(nodeList, true));
|
|
6
13
|
}
|
|
7
14
|
export class PerformanceTimer {
|
package/lib/decision.d.ts
CHANGED
|
@@ -1,2 +1,27 @@
|
|
|
1
1
|
export declare function isSVGElement(nodeName: string): boolean;
|
|
2
2
|
export declare function isPotentialCustomElementName(tagName: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Whether `name` is a reference to a component, not the name of an element, judged by
|
|
5
|
+
* its form alone.
|
|
6
|
+
*
|
|
7
|
+
* The scanners of `pretenders.auto` / `pretenders.scan` follow what a component
|
|
8
|
+
* renders until they reach a name they cannot resolve (a component of a package, a
|
|
9
|
+
* member expression such as `Tabs.Root`, a local variable, a namespace import, a
|
|
10
|
+
* component past the depth limit), and emit that name unchanged. Such a name is never
|
|
11
|
+
* an element: the names of HTML, SVG and MathML elements start with a lowercase
|
|
12
|
+
* letter and have no `.` (the camelCase of `foreignObject` starts with one), and the
|
|
13
|
+
* `.` is valid only in a custom element name, which a hyphen and a lowercase start
|
|
14
|
+
* give away. So the form of the name tells, however the chain stopped.
|
|
15
|
+
*
|
|
16
|
+
* This is the form that most languages share, and the default of `Parser#isComponentName`.
|
|
17
|
+
* A parser of a language that names its components otherwise overrides that method, and what asks
|
|
18
|
+
* (the scanners, `ml-core`, `pretenders`) asks the parser, not this function. It is called by name for a
|
|
19
|
+
* parser that says nothing and for a file of a language that has no parser to ask.
|
|
20
|
+
*
|
|
21
|
+
* The case sensitivity of the document is not part of the criterion, nor is the user's pattern for
|
|
22
|
+
* authored names (`authoredElementName`): the scanners and ml-core must agree on the rule of the language.
|
|
23
|
+
*
|
|
24
|
+
* @param name - A tag name as written, case kept
|
|
25
|
+
* @returns Whether the name is a component's
|
|
26
|
+
*/
|
|
27
|
+
export declare function isComponentName(name: string): boolean;
|
package/lib/decision.js
CHANGED
|
@@ -7,3 +7,30 @@ const isCEN = isCustomElementName();
|
|
|
7
7
|
export function isPotentialCustomElementName(tagName) {
|
|
8
8
|
return isCEN(tagName);
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Whether `name` is a reference to a component, not the name of an element, judged by
|
|
12
|
+
* its form alone.
|
|
13
|
+
*
|
|
14
|
+
* The scanners of `pretenders.auto` / `pretenders.scan` follow what a component
|
|
15
|
+
* renders until they reach a name they cannot resolve (a component of a package, a
|
|
16
|
+
* member expression such as `Tabs.Root`, a local variable, a namespace import, a
|
|
17
|
+
* component past the depth limit), and emit that name unchanged. Such a name is never
|
|
18
|
+
* an element: the names of HTML, SVG and MathML elements start with a lowercase
|
|
19
|
+
* letter and have no `.` (the camelCase of `foreignObject` starts with one), and the
|
|
20
|
+
* `.` is valid only in a custom element name, which a hyphen and a lowercase start
|
|
21
|
+
* give away. So the form of the name tells, however the chain stopped.
|
|
22
|
+
*
|
|
23
|
+
* This is the form that most languages share, and the default of `Parser#isComponentName`.
|
|
24
|
+
* A parser of a language that names its components otherwise overrides that method, and what asks
|
|
25
|
+
* (the scanners, `ml-core`, `pretenders`) asks the parser, not this function. It is called by name for a
|
|
26
|
+
* parser that says nothing and for a file of a language that has no parser to ask.
|
|
27
|
+
*
|
|
28
|
+
* The case sensitivity of the document is not part of the criterion, nor is the user's pattern for
|
|
29
|
+
* authored names (`authoredElementName`): the scanners and ml-core must agree on the rule of the language.
|
|
30
|
+
*
|
|
31
|
+
* @param name - A tag name as written, case kept
|
|
32
|
+
* @returns Whether the name is a component's
|
|
33
|
+
*/
|
|
34
|
+
export function isComponentName(name) {
|
|
35
|
+
return /^[A-Z]|\./.test(name) && !isPotentialCustomElementName(name);
|
|
36
|
+
}
|
|
@@ -20,14 +20,25 @@ function _distinguishAuthoredName(name, patterns) {
|
|
|
20
20
|
if (typeof pattern === 'function') {
|
|
21
21
|
return pattern(name);
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
if (typeof pattern === 'string') {
|
|
24
|
+
const regex = toRegexp(pattern);
|
|
25
|
+
return regex ? regex.test(name) : name === pattern;
|
|
26
|
+
}
|
|
27
|
+
return pattern.test(name);
|
|
25
28
|
});
|
|
26
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* A string in the form of a regular expression literal (`/^[A-Z]/i`, with the flags `g` and `i` at most) is that regular
|
|
32
|
+
* expression. Any other string is a name, and stands for the whole name only: `component`
|
|
33
|
+
* is `<component>`, not `<my-component>` nor `<components>`, and a character such as `.`
|
|
34
|
+
* is that character. So a name is written as it is, as in the examples of `authoredElementName`.
|
|
35
|
+
*
|
|
36
|
+
* @returns The regular expression, or `null` for a name
|
|
37
|
+
*/
|
|
27
38
|
function toRegexp(pattern) {
|
|
28
39
|
const matched = pattern.match(/^\/(.+)\/([gi]*)$/i);
|
|
29
40
|
if (matched && matched[1]) {
|
|
30
41
|
return new RegExp(matched[1], matched[2]);
|
|
31
42
|
}
|
|
32
|
-
return
|
|
43
|
+
return null;
|
|
33
44
|
}
|
package/lib/idl-attributes.d.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Searches the IDL-to-content-attribute mapping to find the corresponding
|
|
3
3
|
* IDL property name and content attribute name for a given attribute name.
|
|
4
|
-
* Handles camelCase IDL names, lowercase content attribute names, hyphenated names,
|
|
5
|
-
* and event handler attributes (e.g., `onclick`).
|
|
6
4
|
*
|
|
7
|
-
*
|
|
5
|
+
* A name is mapped by its exact spelling: the IDL property name (`tabIndex`, `strokeWidth`) or the
|
|
6
|
+
* content attribute name (`tabindex`, `stroke-width`). A spelling in another case (`TABINDEX`) is
|
|
7
|
+
* another name here, because a framework reads its own property names as they are written (JSX
|
|
8
|
+
* `onClick` is a JavaScript identifier). Whether such a name is still the same attribute in the DOM is
|
|
9
|
+
* decided by `MLAttr#isNamedBy` of `@markuplint/ml-core`, not here. An event handler
|
|
10
|
+
* (`onclick`) is its own name in lower case.
|
|
11
|
+
*
|
|
12
|
+
* @param name - The attribute name to look up (the IDL or the content spelling, exactly)
|
|
8
13
|
* @returns An object with the matching `idlPropName` and `contentAttrName`, both undefined if no match is found
|
|
9
14
|
*/
|
|
10
15
|
export declare function searchIDLAttribute(name: string): {
|
|
11
16
|
idlPropName: string | undefined;
|
|
12
17
|
contentAttrName: string | undefined;
|
|
13
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Finds the IDL property that a name suggests whatever the case it is written in: `TABINDEX`,
|
|
21
|
+
* `tab-index` and `tabindex` suggest `tabIndex`. It is for a message that says how the framework
|
|
22
|
+
* spells the name (a suggestion), not for the name of an attribute: that is
|
|
23
|
+
* {@link searchIDLAttribute}, which reads a spelling exactly.
|
|
24
|
+
*
|
|
25
|
+
* @param name - The attribute name, in any case
|
|
26
|
+
* @returns The IDL property name, undefined if no name of the mapping is like it
|
|
27
|
+
*/
|
|
28
|
+
export declare function suggestIDLAttribute(name: string): string | undefined;
|
package/lib/idl-attributes.js
CHANGED
|
@@ -416,30 +416,64 @@ const idlContentMap = {
|
|
|
416
416
|
credentialless: 'credentialless',
|
|
417
417
|
};
|
|
418
418
|
const list = Object.entries(idlContentMap);
|
|
419
|
+
// The first entry of a name wins. They are built once: a name is looked up for every attribute of a document.
|
|
420
|
+
const byIdlPropName = new Map();
|
|
421
|
+
const byContentAttrName = new Map();
|
|
422
|
+
for (const entry of list) {
|
|
423
|
+
if (!byIdlPropName.has(entry[0])) {
|
|
424
|
+
byIdlPropName.set(entry[0], entry);
|
|
425
|
+
}
|
|
426
|
+
if (!byContentAttrName.has(entry[1])) {
|
|
427
|
+
byContentAttrName.set(entry[1], entry);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
419
430
|
/**
|
|
420
431
|
* Searches the IDL-to-content-attribute mapping to find the corresponding
|
|
421
432
|
* IDL property name and content attribute name for a given attribute name.
|
|
422
|
-
* Handles camelCase IDL names, lowercase content attribute names, hyphenated names,
|
|
423
|
-
* and event handler attributes (e.g., `onclick`).
|
|
424
433
|
*
|
|
425
|
-
*
|
|
434
|
+
* A name is mapped by its exact spelling: the IDL property name (`tabIndex`, `strokeWidth`) or the
|
|
435
|
+
* content attribute name (`tabindex`, `stroke-width`). A spelling in another case (`TABINDEX`) is
|
|
436
|
+
* another name here, because a framework reads its own property names as they are written (JSX
|
|
437
|
+
* `onClick` is a JavaScript identifier). Whether such a name is still the same attribute in the DOM is
|
|
438
|
+
* decided by `MLAttr#isNamedBy` of `@markuplint/ml-core`, not here. An event handler
|
|
439
|
+
* (`onclick`) is its own name in lower case.
|
|
440
|
+
*
|
|
441
|
+
* @param name - The attribute name to look up (the IDL or the content spelling, exactly)
|
|
426
442
|
* @returns An object with the matching `idlPropName` and `contentAttrName`, both undefined if no match is found
|
|
427
443
|
*/
|
|
428
444
|
export function searchIDLAttribute(name) {
|
|
429
|
-
const camelizedName = camelize(name);
|
|
430
445
|
const [idlPropName, contentAttrName] = /^on[a-z]/.test(name)
|
|
431
446
|
? [name.toLowerCase(), name.toLowerCase()]
|
|
432
|
-
: (
|
|
433
|
-
contentAttrName.toLowerCase() === name.toLowerCase() ||
|
|
434
|
-
hyphenize(idlPropName) === name.toLowerCase()) ?? []);
|
|
447
|
+
: (byIdlPropName.get(name) ?? byContentAttrName.get(name) ?? []);
|
|
435
448
|
return {
|
|
436
449
|
idlPropName,
|
|
437
450
|
contentAttrName,
|
|
438
451
|
};
|
|
439
452
|
}
|
|
440
|
-
|
|
441
|
-
|
|
453
|
+
// The IDL property that a name of another spelling suggests, by the lower-case of its IDL name, of its
|
|
454
|
+
// content name, and of its hyphenated form. The first entry of the list wins.
|
|
455
|
+
const suggestions = new Map();
|
|
456
|
+
for (const [idlPropName, contentAttrName] of list) {
|
|
457
|
+
for (const key of [
|
|
458
|
+
idlPropName.toLowerCase(),
|
|
459
|
+
contentAttrName.toLowerCase(),
|
|
460
|
+
idlPropName.replaceAll(/[A-Z]/g, c => `-${c.toLowerCase()}`),
|
|
461
|
+
]) {
|
|
462
|
+
if (!suggestions.has(key)) {
|
|
463
|
+
suggestions.set(key, idlPropName);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
442
466
|
}
|
|
443
|
-
|
|
444
|
-
|
|
467
|
+
/**
|
|
468
|
+
* Finds the IDL property that a name suggests whatever the case it is written in: `TABINDEX`,
|
|
469
|
+
* `tab-index` and `tabindex` suggest `tabIndex`. It is for a message that says how the framework
|
|
470
|
+
* spells the name (a suggestion), not for the name of an attribute: that is
|
|
471
|
+
* {@link searchIDLAttribute}, which reads a spelling exactly.
|
|
472
|
+
*
|
|
473
|
+
* @param name - The attribute name, in any case
|
|
474
|
+
* @returns The IDL property name, undefined if no name of the mapping is like it
|
|
475
|
+
*/
|
|
476
|
+
export function suggestIDLAttribute(name) {
|
|
477
|
+
const lower = name.toLowerCase();
|
|
478
|
+
return suggestions.get(lower) ?? suggestions.get(lower.replaceAll(/[:-]/g, ''));
|
|
445
479
|
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/lib/parser.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Token, ChildToken, QuoteSet, ParseOptions, ParserOptions, Tokenized, ValueType } from './types.js';
|
|
2
|
-
import type { EndTagType, MLASTDocument, MLASTParentNode, MLParser, ParserAuthoredElementNameDistinguishing, MLASTElement, MLASTElementCloseTag, MLASTToken, MLASTNodeTreeItem, MLASTTag, MLASTText, MLASTAttr, MLASTChildNode, MLASTSpreadAttr, ElementType, Walker, MLASTHTMLAttr, MLASTBlockBehavior, MLASTParseError } from '@markuplint/ml-ast';
|
|
2
|
+
import type { AttrNameRoute, EndTagType, MLASTDocument, MLASTParentNode, MLParser, ParserAuthoredElementNameDistinguishing, MLASTElement, MLASTElementCloseTag, MLASTToken, MLASTNodeTreeItem, MLASTTag, MLASTText, MLASTAttr, MLASTChildNode, MLASTSpreadAttr, ElementType, Walker, MLASTHTMLAttr, MLASTBlockBehavior, MLASTParseError } from '@markuplint/ml-ast';
|
|
3
3
|
import { AttrState } from './enums.js';
|
|
4
4
|
import { ParserError } from './parser-error.js';
|
|
5
5
|
/**
|
|
@@ -73,6 +73,27 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
|
|
|
73
73
|
* When false (the default), tag name comparisons are case-insensitive (HTML behavior).
|
|
74
74
|
*/
|
|
75
75
|
get tagNameCaseSensitive(): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Whether a component is referred to by any spelling of its name (`<FooBar>`, `<fooBar>`
|
|
78
|
+
* and `<foo-bar>` are one component). Only Vue resolves a tag so; see
|
|
79
|
+
* {@link MLParser.componentNameVariants} in `@markuplint/ml-ast`.
|
|
80
|
+
*/
|
|
81
|
+
get componentNameVariants(): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Whether attribute names are case-sensitive in this language (JSX props such as
|
|
84
|
+
* `onClick`). Independent of {@link Parser.tagNameCaseSensitive}: Svelte and Astro
|
|
85
|
+
* distinguish components by tag-name case but lowercase or pass through HTML
|
|
86
|
+
* attribute names, so they leave this `false`.
|
|
87
|
+
*/
|
|
88
|
+
get attrNameCaseSensitive(): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* How the name of an attribute gets into the DOM of the browser (see `AttrNameRoute`), which decides
|
|
91
|
+
* whether another spelling of the name is the same attribute. Independent of
|
|
92
|
+
* {@link Parser.attrNameCaseSensitive}, which is about the style rules: Vue sets its attributes through
|
|
93
|
+
* the DOM API, but its names are not JavaScript identifiers, so a template is still checked for the
|
|
94
|
+
* case of a name. The HTML parser of the browser by default.
|
|
95
|
+
*/
|
|
96
|
+
get attrNameRoute(): AttrNameRoute;
|
|
76
97
|
/**
|
|
77
98
|
* Tokenizes the raw source code into language-specific AST nodes.
|
|
78
99
|
* Subclasses should override this method to provide actual tokenization logic.
|
|
@@ -264,7 +285,14 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
|
|
|
264
285
|
/**
|
|
265
286
|
* Traverses a list of child nodes under the given parent, appending the resulting
|
|
266
287
|
* child AST nodes to the parent and returning any sibling nodes that belong
|
|
267
|
-
* to ancestor levels.
|
|
288
|
+
* to ancestor levels.
|
|
289
|
+
*
|
|
290
|
+
* Children of a raw-text element (`rawTextElements`; `style` and `script` by
|
|
291
|
+
* default) are dropped on purpose. Their body is CSS or JavaScript, not markup,
|
|
292
|
+
* so it gets no text node and the element reads as empty downstream
|
|
293
|
+
* (`textContent === ''`, no `childNodes`, no `Text` visit). A rule that needs the
|
|
294
|
+
* body reads it from the document source between the element's `endOffset` and
|
|
295
|
+
* its `closeTag.startOffset`, when a close tag is present.
|
|
268
296
|
*
|
|
269
297
|
* @param children - The language-specific child AST nodes to traverse
|
|
270
298
|
* @param parentNode - The parent markuplint AST node to which children will be appended
|
|
@@ -366,10 +394,42 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
|
|
|
366
394
|
* given tag name, using the parser's authored element name distinguishing pattern.
|
|
367
395
|
*
|
|
368
396
|
* @param nodeName - The tag name to classify
|
|
369
|
-
* @param defaultPattern - A fallback pattern if no authored element name pattern is set
|
|
397
|
+
* @param defaultPattern - A fallback pattern if no authored element name pattern is set: a regular
|
|
398
|
+
* expression, a name, or a function (a parser of a language that decides by a rule passes
|
|
399
|
+
* `name => this.isComponentName(name)`)
|
|
370
400
|
* @returns The element type classification
|
|
371
401
|
*/
|
|
372
402
|
detectElementType(nodeName: string, defaultPattern?: ParserAuthoredElementNameDistinguishing): ElementType;
|
|
403
|
+
/**
|
|
404
|
+
* Whether a character can be the first of a tag name, where the tag scanner reads a `<` followed by it as a
|
|
405
|
+
* start tag. Anything else after a `<` leaves it as text (`a < b`, `<3`).
|
|
406
|
+
*
|
|
407
|
+
* The default is an ASCII letter, the rule of the HTML tokenizer ("tag open state") that most
|
|
408
|
+
* languages share. A parser of a language whose element names start otherwise overrides this
|
|
409
|
+
* (JSX: `$`, `_` and a letter of any script).
|
|
410
|
+
*
|
|
411
|
+
* @param char - The character that follows `<`
|
|
412
|
+
* @returns Whether it starts a tag name
|
|
413
|
+
* @see https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
|
|
414
|
+
*/
|
|
415
|
+
isTagNameStart(char: string): boolean;
|
|
416
|
+
/**
|
|
417
|
+
* Whether a name is a reference to a component, not the name of an element, judged by its form
|
|
418
|
+
* alone in the language this parser reads. The scanners of `pretenders` end a chain of what a
|
|
419
|
+
* component renders at a name they cannot resolve (`Tabs.Root`, a variable) and emit it as it is,
|
|
420
|
+
* and a pretender's target (`as.element`) is typed by it, so what the scanners and `ml-core` take
|
|
421
|
+
* for a component has to be the parser's own answer.
|
|
422
|
+
*
|
|
423
|
+
* The default is the form that most languages share: a capital initial or a `.` in a name that
|
|
424
|
+
* is not that of a custom element (the `isComponentName` function of `./decision.ts`). A parser of a language that
|
|
425
|
+
* names its components otherwise overrides this, as it overrides {@link detectElementType}; this
|
|
426
|
+
* class names no language. The user's `authoredElementName` is not part of the answer: it is
|
|
427
|
+
* the user's choice of what to call an authored element in a document, not the language's rule.
|
|
428
|
+
*
|
|
429
|
+
* @param name - A tag name as written, case kept
|
|
430
|
+
* @returns Whether the name is a component's
|
|
431
|
+
*/
|
|
432
|
+
isComponentName(name: string): boolean;
|
|
373
433
|
/**
|
|
374
434
|
* Creates a new MLASTToken with a generated UUID.
|
|
375
435
|
* Accepts either a Token object or a raw string with explicit start coordinates.
|
package/lib/parser.js
CHANGED
|
@@ -3,6 +3,7 @@ import { isVoidElement as detectVoidElement } from '@markuplint/ml-spec';
|
|
|
3
3
|
import { attrTokenizer } from './attr-tokenizer.js';
|
|
4
4
|
import { defaultSpaces } from './const.js';
|
|
5
5
|
import { domLog, PerformanceTimer } from './debug.js';
|
|
6
|
+
import { isComponentName } from './decision.js';
|
|
6
7
|
import { detectElementType } from './detect-element-type.js';
|
|
7
8
|
import { AttrState, TagState } from './enums.js';
|
|
8
9
|
import { getEndCol, getEndLine, getOffsetsFromCode, getPosition } from './get-location.js';
|
|
@@ -46,6 +47,9 @@ export class Parser {
|
|
|
46
47
|
#ignoreTags = [];
|
|
47
48
|
#maskChar;
|
|
48
49
|
#tagNameCaseSensitive = false;
|
|
50
|
+
#componentNameVariants = false;
|
|
51
|
+
#attrNameCaseSensitive = false;
|
|
52
|
+
#attrNameRoute = 'html-parser';
|
|
49
53
|
#selfCloseType = 'html';
|
|
50
54
|
#spaceChars = defaultSpaces;
|
|
51
55
|
#rawTextElements = ['style', 'script'];
|
|
@@ -86,6 +90,9 @@ export class Parser {
|
|
|
86
90
|
this.#ignoreTags = options?.ignoreTags ?? this.#ignoreTags;
|
|
87
91
|
this.#maskChar = options?.maskChar ?? this.#maskChar;
|
|
88
92
|
this.#tagNameCaseSensitive = options?.tagNameCaseSensitive ?? this.#tagNameCaseSensitive;
|
|
93
|
+
this.#componentNameVariants = options?.componentNameVariants ?? this.#componentNameVariants;
|
|
94
|
+
this.#attrNameCaseSensitive = options?.attrNameCaseSensitive ?? this.#attrNameCaseSensitive;
|
|
95
|
+
this.#attrNameRoute = options?.attrNameRoute ?? this.#attrNameRoute;
|
|
89
96
|
this.#selfCloseType = options?.selfCloseType ?? this.#selfCloseType;
|
|
90
97
|
this.#spaceChars = options?.spaceChars ?? this.#spaceChars;
|
|
91
98
|
this.#rawTextElements = options?.rawTextElements ?? this.#rawTextElements;
|
|
@@ -136,6 +143,33 @@ export class Parser {
|
|
|
136
143
|
get tagNameCaseSensitive() {
|
|
137
144
|
return this.#tagNameCaseSensitive;
|
|
138
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Whether a component is referred to by any spelling of its name (`<FooBar>`, `<fooBar>`
|
|
148
|
+
* and `<foo-bar>` are one component). Only Vue resolves a tag so; see
|
|
149
|
+
* {@link MLParser.componentNameVariants} in `@markuplint/ml-ast`.
|
|
150
|
+
*/
|
|
151
|
+
get componentNameVariants() {
|
|
152
|
+
return this.#componentNameVariants;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Whether attribute names are case-sensitive in this language (JSX props such as
|
|
156
|
+
* `onClick`). Independent of {@link Parser.tagNameCaseSensitive}: Svelte and Astro
|
|
157
|
+
* distinguish components by tag-name case but lowercase or pass through HTML
|
|
158
|
+
* attribute names, so they leave this `false`.
|
|
159
|
+
*/
|
|
160
|
+
get attrNameCaseSensitive() {
|
|
161
|
+
return this.#attrNameCaseSensitive;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* How the name of an attribute gets into the DOM of the browser (see `AttrNameRoute`), which decides
|
|
165
|
+
* whether another spelling of the name is the same attribute. Independent of
|
|
166
|
+
* {@link Parser.attrNameCaseSensitive}, which is about the style rules: Vue sets its attributes through
|
|
167
|
+
* the DOM API, but its names are not JavaScript identifiers, so a template is still checked for the
|
|
168
|
+
* case of a name. The HTML parser of the browser by default.
|
|
169
|
+
*/
|
|
170
|
+
get attrNameRoute() {
|
|
171
|
+
return this.#attrNameRoute;
|
|
172
|
+
}
|
|
139
173
|
/**
|
|
140
174
|
* Tokenizes the raw source code into language-specific AST nodes.
|
|
141
175
|
* Subclasses should override this method to provide actual tokenization logic.
|
|
@@ -576,7 +610,14 @@ export class Parser {
|
|
|
576
610
|
/**
|
|
577
611
|
* Traverses a list of child nodes under the given parent, appending the resulting
|
|
578
612
|
* child AST nodes to the parent and returning any sibling nodes that belong
|
|
579
|
-
* to ancestor levels.
|
|
613
|
+
* to ancestor levels.
|
|
614
|
+
*
|
|
615
|
+
* Children of a raw-text element (`rawTextElements`; `style` and `script` by
|
|
616
|
+
* default) are dropped on purpose. Their body is CSS or JavaScript, not markup,
|
|
617
|
+
* so it gets no text node and the element reads as empty downstream
|
|
618
|
+
* (`textContent === ''`, no `childNodes`, no `Text` visit). A rule that needs the
|
|
619
|
+
* body reads it from the document source between the element's `endOffset` and
|
|
620
|
+
* its `closeTag.startOffset`, when a close tag is present.
|
|
580
621
|
*
|
|
581
622
|
* @param children - The language-specific child AST nodes to traverse
|
|
582
623
|
* @param parentNode - The parent markuplint AST node to which children will be appended
|
|
@@ -896,12 +937,48 @@ export class Parser {
|
|
|
896
937
|
* given tag name, using the parser's authored element name distinguishing pattern.
|
|
897
938
|
*
|
|
898
939
|
* @param nodeName - The tag name to classify
|
|
899
|
-
* @param defaultPattern - A fallback pattern if no authored element name pattern is set
|
|
940
|
+
* @param defaultPattern - A fallback pattern if no authored element name pattern is set: a regular
|
|
941
|
+
* expression, a name, or a function (a parser of a language that decides by a rule passes
|
|
942
|
+
* `name => this.isComponentName(name)`)
|
|
900
943
|
* @returns The element type classification
|
|
901
944
|
*/
|
|
902
945
|
detectElementType(nodeName, defaultPattern) {
|
|
903
946
|
return detectElementType(nodeName, this.#authoredElementName, defaultPattern);
|
|
904
947
|
}
|
|
948
|
+
/**
|
|
949
|
+
* Whether a character can be the first of a tag name, where the tag scanner reads a `<` followed by it as a
|
|
950
|
+
* start tag. Anything else after a `<` leaves it as text (`a < b`, `<3`).
|
|
951
|
+
*
|
|
952
|
+
* The default is an ASCII letter, the rule of the HTML tokenizer ("tag open state") that most
|
|
953
|
+
* languages share. A parser of a language whose element names start otherwise overrides this
|
|
954
|
+
* (JSX: `$`, `_` and a letter of any script).
|
|
955
|
+
*
|
|
956
|
+
* @param char - The character that follows `<`
|
|
957
|
+
* @returns Whether it starts a tag name
|
|
958
|
+
* @see https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
|
|
959
|
+
*/
|
|
960
|
+
isTagNameStart(char) {
|
|
961
|
+
return /[a-z]/i.test(char);
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Whether a name is a reference to a component, not the name of an element, judged by its form
|
|
965
|
+
* alone in the language this parser reads. The scanners of `pretenders` end a chain of what a
|
|
966
|
+
* component renders at a name they cannot resolve (`Tabs.Root`, a variable) and emit it as it is,
|
|
967
|
+
* and a pretender's target (`as.element`) is typed by it, so what the scanners and `ml-core` take
|
|
968
|
+
* for a component has to be the parser's own answer.
|
|
969
|
+
*
|
|
970
|
+
* The default is the form that most languages share: a capital initial or a `.` in a name that
|
|
971
|
+
* is not that of a custom element (the `isComponentName` function of `./decision.ts`). A parser of a language that
|
|
972
|
+
* names its components otherwise overrides this, as it overrides {@link detectElementType}; this
|
|
973
|
+
* class names no language. The user's `authoredElementName` is not part of the answer: it is
|
|
974
|
+
* the user's choice of what to call an authored element in a document, not the language's rule.
|
|
975
|
+
*
|
|
976
|
+
* @param name - A tag name as written, case kept
|
|
977
|
+
* @returns Whether the name is a component's
|
|
978
|
+
*/
|
|
979
|
+
isComponentName(name) {
|
|
980
|
+
return isComponentName(name);
|
|
981
|
+
}
|
|
905
982
|
createToken(token, offset, line, col) {
|
|
906
983
|
const props = typeof token === 'string'
|
|
907
984
|
? {
|
|
@@ -1251,7 +1328,7 @@ export class Parser {
|
|
|
1251
1328
|
break;
|
|
1252
1329
|
}
|
|
1253
1330
|
case TagState.FirstCharOfTagName: {
|
|
1254
|
-
if (
|
|
1331
|
+
if (this.isTagNameStart(char)) {
|
|
1255
1332
|
tagName += char;
|
|
1256
1333
|
state = TagState.TagName;
|
|
1257
1334
|
break;
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EndTagType, MLASTParentNode, MLASTParseError, ParserOptions as ConfigParserOptions } from '@markuplint/ml-ast';
|
|
1
|
+
import type { AttrNameRoute, EndTagType, MLASTParentNode, MLASTParseError, ParserOptions as ConfigParserOptions } from '@markuplint/ml-ast';
|
|
2
2
|
/**
|
|
3
3
|
* Configuration options for initializing a Parser instance,
|
|
4
4
|
* controlling how the parser handles tags, attributes, and whitespace.
|
|
@@ -9,6 +9,9 @@ export type ParserOptions = {
|
|
|
9
9
|
readonly ignoreTags?: readonly IgnoreTag[];
|
|
10
10
|
readonly maskChar?: string;
|
|
11
11
|
readonly tagNameCaseSensitive?: boolean;
|
|
12
|
+
readonly componentNameVariants?: boolean;
|
|
13
|
+
readonly attrNameCaseSensitive?: boolean;
|
|
14
|
+
readonly attrNameRoute?: AttrNameRoute;
|
|
12
15
|
readonly selfCloseType?: SelfCloseType;
|
|
13
16
|
readonly spaceChars?: readonly string[];
|
|
14
17
|
readonly rawTextElements?: readonly string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/parser-utils",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "Utility module for markuplint parser plugin",
|
|
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
|
"./location": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"types": "./lib/get-location.d.ts",
|
|
23
|
+
"default": "./lib/get-location.js"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
@@ -35,16 +35,16 @@
|
|
|
35
35
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@markuplint/ml-ast": "5.
|
|
39
|
-
"@markuplint/ml-spec": "5.
|
|
40
|
-
"@markuplint/shared": "5.
|
|
41
|
-
"@markuplint/types": "5.
|
|
38
|
+
"@markuplint/ml-ast": "5.3.0",
|
|
39
|
+
"@markuplint/ml-spec": "5.4.0",
|
|
40
|
+
"@markuplint/shared": "5.4.0",
|
|
41
|
+
"@markuplint/types": "5.4.0",
|
|
42
42
|
"debug": "^4.4.3",
|
|
43
43
|
"espree": "^11.2.0",
|
|
44
44
|
"type-fest": "^5.10.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@typescript-eslint/typescript-estree": "8.71.
|
|
47
|
+
"@typescript-eslint/typescript-estree": "8.71.2"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "ab56aa8e12b67fbf793ddf1267aaba9fda90269d"
|
|
50
50
|
}
|