@markuplint/ml-core 5.2.0 → 5.3.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.
@@ -10,7 +10,7 @@ import type { Ruleset } from '../../ruleset/index.js';
10
10
  import type { MLSchema } from '../../types.js';
11
11
  import type { Walker } from '../helper/walkers.js';
12
12
  import type { MLToken } from '../token/token.js';
13
- import type { EndTagType, MLASTDocument, MLASTNodeTreeItem, MLASTParseError } from '@markuplint/ml-ast';
13
+ import type { AttrNameRoute, EndTagType, MLASTDocument, MLASTNodeTreeItem, MLASTParseError } from '@markuplint/ml-ast';
14
14
  import type { PlainData, Pretender, RuleCommonSettings, RuleConfigValue } from '@markuplint/ml-config';
15
15
  import type { ARIAVersion, MLMLSpec } from '@markuplint/ml-spec';
16
16
  import { MLParentNode } from './parent-node.js';
@@ -74,6 +74,53 @@ export declare class MLDocument<T extends RuleConfigValue, O extends PlainData =
74
74
  * When false (default for HTML), tag names are compared case-insensitively.
75
75
  */
76
76
  readonly tagNameCaseSensitive: boolean;
77
+ /**
78
+ * Whether a component is referred to by any spelling of its name: `<FooBar>`, `<fooBar>` and
79
+ * `<foo-bar>` are one component (Vue). A type selector reads it through
80
+ * {@link MLElement.tagNameKind}.
81
+ */
82
+ readonly componentNameVariants: boolean;
83
+ /**
84
+ * Whether a name is a reference to a component, not the name of an element, judged by its form
85
+ * alone in the language of the parser (`Tabs.Root` is, `div` is not). The target of a pretender
86
+ * is typed by it. It is the rule of the parser (`MLParser#isComponentName`, a method of the parser's
87
+ * class, not of this one); the form that most languages share (`isComponentName` of
88
+ * `@markuplint/parser-utils`) when the parser has none.
89
+ */
90
+ readonly isComponentName: (name: string) => boolean;
91
+ /**
92
+ * Whether a tag is a native element: the rule of the parser (`MLParser#isNativeTag`) when it has one, which
93
+ * takes the name as written and knows no namespace, else the specification (`getSpecByTagName`) of the
94
+ * name in its namespace. {@link MLElement.tagNameKind} and the guard of {@link MLElement.pretending}
95
+ * read it, so that a type selector and a pretender agree on what a tag is.
96
+ *
97
+ * Rules keyed on the specification (deprecated / obsolete elements) do not read it. Nor does the
98
+ * element that a pretender stands for (`as`): the name is the element itself there, not a tag to resolve,
99
+ * so it is judged by the specification and never passes through this method.
100
+ *
101
+ * @param localName - The name of the tag as the author wrote it, never the `as` of a pretender
102
+ * @param namespaceURI - The namespace of the element; not read when the parser has a rule, as Vue's is the same for every namespace
103
+ * @returns Whether the document takes the tag for a native element
104
+ */
105
+ isNativeTag(localName: string, namespaceURI: string): boolean;
106
+ /**
107
+ * Whether attribute names are JavaScript identifiers in the source language (JSX props such as
108
+ * `onClick`), which the `case-sensitive-attr-name` rule does not ask to be lower case.
109
+ *
110
+ * It says nothing about whether two spellings of a name are one attribute: that is up to how the
111
+ * name gets into the DOM ({@link MLDocument.attrNameRoute}) and to the element, and is decided by
112
+ * {@link MLAttr.nameKey}. Svelte and Astro tell the case of a tag name apart but not of an
113
+ * attribute name, and Vue sets its attributes through the DOM API though a template is still
114
+ * checked for the case of a name.
115
+ */
116
+ readonly attrNameCaseSensitive: boolean;
117
+ /**
118
+ * How the name of an attribute gets into the DOM of the browser (`MLParser#attrNameRoute`, see
119
+ * `AttrNameRoute`). It decides whether `VIEWBOX` and `viewBox` are one attribute, which is a matter of the
120
+ * DOM and so of the element and the attribute as well; this is what the parser says about its language.
121
+ * `"html-parser"` when the parser does not say.
122
+ */
123
+ readonly attrNameRoute: AttrNameRoute;
77
124
  /**
78
125
  * Common settings applied globally to all rules, such as the ARIA version.
79
126
  * Rules use this as a fallback when their own options do not specify a value.
@@ -91,6 +138,11 @@ export declare class MLDocument<T extends RuleConfigValue, O extends PlainData =
91
138
  readonly endTag?: 'xml' | 'omittable' | 'never';
92
139
  readonly booleanish?: boolean;
93
140
  readonly tagNameCaseSensitive?: boolean;
141
+ readonly componentNameVariants?: boolean;
142
+ readonly isComponentName?: (name: string) => boolean;
143
+ readonly isNativeTag?: (name: string) => boolean;
144
+ readonly attrNameCaseSensitive?: boolean;
145
+ readonly attrNameRoute?: AttrNameRoute;
94
146
  readonly pretenders?: readonly Pretender[];
95
147
  });
96
148
  /**
@@ -1,6 +1,6 @@
1
1
  import { exchangeValueOnRule, mergeRule } from '@markuplint/ml-config';
2
- import { schemaToSpec, getComputedRole, mayBeFocusable, getComputedAriaProps, isExposed, ARIA_RECOMMENDED_VERSION, } from '@markuplint/ml-spec';
3
- import { ConfigParserError } from '@markuplint/parser-utils';
2
+ import { schemaToSpec, getComputedRole, mayBeFocusable, getComputedAriaProps, isExposed, getSpecByTagName, ARIA_RECOMMENDED_VERSION, } from '@markuplint/ml-spec';
3
+ import { ConfigParserError, isComponentName } from '@markuplint/parser-utils';
4
4
  import { InvalidSelectorError } from '@markuplint/selector';
5
5
  import { log as coreLog } from '../../debug.js';
6
6
  import { createNode } from '../helper/create-node.js';
@@ -50,6 +50,7 @@ export class MLDocument extends MLParentNode {
50
50
  endTag;
51
51
  #astNodeMap;
52
52
  #filename;
53
+ #isNativeTagRule;
53
54
  /**
54
55
  * Whether this document represents a fragment rather than a complete document.
55
56
  * Fragment documents may lack root-level elements like `<html>`, `<head>`, or `<body>`.
@@ -74,6 +75,58 @@ export class MLDocument extends MLParentNode {
74
75
  * When false (default for HTML), tag names are compared case-insensitively.
75
76
  */
76
77
  tagNameCaseSensitive;
78
+ /**
79
+ * Whether a component is referred to by any spelling of its name: `<FooBar>`, `<fooBar>` and
80
+ * `<foo-bar>` are one component (Vue). A type selector reads it through
81
+ * {@link MLElement.tagNameKind}.
82
+ */
83
+ componentNameVariants;
84
+ /**
85
+ * Whether a name is a reference to a component, not the name of an element, judged by its form
86
+ * alone in the language of the parser (`Tabs.Root` is, `div` is not). The target of a pretender
87
+ * is typed by it. It is the rule of the parser (`MLParser#isComponentName`, a method of the parser's
88
+ * class, not of this one); the form that most languages share (`isComponentName` of
89
+ * `@markuplint/parser-utils`) when the parser has none.
90
+ */
91
+ isComponentName;
92
+ /**
93
+ * Whether a tag is a native element: the rule of the parser (`MLParser#isNativeTag`) when it has one, which
94
+ * takes the name as written and knows no namespace, else the specification (`getSpecByTagName`) of the
95
+ * name in its namespace. {@link MLElement.tagNameKind} and the guard of {@link MLElement.pretending}
96
+ * read it, so that a type selector and a pretender agree on what a tag is.
97
+ *
98
+ * Rules keyed on the specification (deprecated / obsolete elements) do not read it. Nor does the
99
+ * element that a pretender stands for (`as`): the name is the element itself there, not a tag to resolve,
100
+ * so it is judged by the specification and never passes through this method.
101
+ *
102
+ * @param localName - The name of the tag as the author wrote it, never the `as` of a pretender
103
+ * @param namespaceURI - The namespace of the element; not read when the parser has a rule, as Vue's is the same for every namespace
104
+ * @returns Whether the document takes the tag for a native element
105
+ */
106
+ isNativeTag(localName, namespaceURI) {
107
+ if (this.#isNativeTagRule) {
108
+ return this.#isNativeTagRule(localName);
109
+ }
110
+ return getSpecByTagName(this.specs.specs, localName, namespaceURI) != null;
111
+ }
112
+ /**
113
+ * Whether attribute names are JavaScript identifiers in the source language (JSX props such as
114
+ * `onClick`), which the `case-sensitive-attr-name` rule does not ask to be lower case.
115
+ *
116
+ * It says nothing about whether two spellings of a name are one attribute: that is up to how the
117
+ * name gets into the DOM ({@link MLDocument.attrNameRoute}) and to the element, and is decided by
118
+ * {@link MLAttr.nameKey}. Svelte and Astro tell the case of a tag name apart but not of an
119
+ * attribute name, and Vue sets its attributes through the DOM API though a template is still
120
+ * checked for the case of a name.
121
+ */
122
+ attrNameCaseSensitive;
123
+ /**
124
+ * How the name of an attribute gets into the DOM of the browser (`MLParser#attrNameRoute`, see
125
+ * `AttrNameRoute`). It decides whether `VIEWBOX` and `viewBox` are one attribute, which is a matter of the
126
+ * DOM and so of the element and the attribute as well; this is what the parser says about its language.
127
+ * `"html-parser"` when the parser does not say.
128
+ */
129
+ attrNameRoute;
77
130
  /**
78
131
  * Common settings applied globally to all rules, such as the ARIA version.
79
132
  * Rules use this as a fallback when their own options do not specify a value.
@@ -95,6 +148,11 @@ export class MLDocument extends MLParentNode {
95
148
  this.endTag = options?.endTag ?? 'omittable';
96
149
  this.#filename = options?.filename;
97
150
  this.tagNameCaseSensitive = options?.tagNameCaseSensitive ?? false;
151
+ this.componentNameVariants = options?.componentNameVariants ?? false;
152
+ this.isComponentName = options?.isComponentName ?? isComponentName;
153
+ this.#isNativeTagRule = options?.isNativeTag;
154
+ this.attrNameCaseSensitive = options?.attrNameCaseSensitive ?? false;
155
+ this.attrNameRoute = options?.attrNameRoute ?? 'html-parser';
98
156
  this.ruleCommonSettings = ruleCommonSettings;
99
157
  // Build UUID → AST node map for cross-reference lookups (e.g., pairNodeUuid)
100
158
  this.#astNodeMap = new Map(ast.nodeList.map(n => [n.uuid, n]));
@@ -2351,7 +2409,10 @@ export class MLDocument extends MLParentNode {
2351
2409
  }
2352
2410
  }
2353
2411
  #ruleMapping(ruleset) {
2354
- if (docLog.enabled) {
2412
+ // Read once: the per-node loop below would otherwise hit the `enabled` getter for every node.
2413
+ const docEnabled = docLog.enabled;
2414
+ const ruleEnabled = ruleLog.enabled;
2415
+ if (docEnabled) {
2355
2416
  docLog('Rule Mapping: %O', Object.keys(ruleset.rules));
2356
2417
  }
2357
2418
  const ruleMapper = new RuleMapper(this);
@@ -2369,7 +2430,7 @@ export class MLDocument extends MLParentNode {
2369
2430
  }
2370
2431
  // add rules to node
2371
2432
  for (const node of this.nodeList) {
2372
- if (docLog.enabled) {
2433
+ if (docEnabled) {
2373
2434
  docLog('Add rules to node <%s>', node.nodeName);
2374
2435
  }
2375
2436
  // global rules to each element
@@ -2401,7 +2462,7 @@ export class MLDocument extends MLParentNode {
2401
2462
  if (!matches.matched) {
2402
2463
  continue;
2403
2464
  }
2404
- if (docLog.enabled) {
2465
+ if (docEnabled) {
2405
2466
  docLog('Matched nodeRule: <%s>(%s)', node.nodeName, matches.selector || '*');
2406
2467
  }
2407
2468
  const ruleList = Object.keys(nodeRule.rules);
@@ -2434,7 +2495,9 @@ export class MLDocument extends MLParentNode {
2434
2495
  }
2435
2496
  const globalRule = ruleset.rules[ruleName];
2436
2497
  const mergedRule = globalRule == null ? convertedRule : mergeRule(globalRule, convertedRule);
2437
- ruleLog('↑ nodeRule (%s): %O', ruleName, mergedRule);
2498
+ if (ruleEnabled) {
2499
+ ruleLog('↑ nodeRule (%s): %O', ruleName, mergedRule);
2500
+ }
2438
2501
  ruleMapper.set(node, ruleName, {
2439
2502
  from: 'nodeRules',
2440
2503
  specificity: matches.specificity,
@@ -2497,7 +2560,7 @@ export class MLDocument extends MLParentNode {
2497
2560
  if (!matches.matched) {
2498
2561
  continue;
2499
2562
  }
2500
- if (docLog.enabled) {
2563
+ if (docEnabled) {
2501
2564
  docLog('Matched childNodeRule: <%s>(%s), inheritance: %o', selectorTarget.nodeName, matches.selector || '*', !!nodeRule.inheritance);
2502
2565
  }
2503
2566
  const targetDescendants = nodeRule.inheritance ? descendants : children;
@@ -2532,7 +2595,9 @@ export class MLDocument extends MLParentNode {
2532
2595
  }
2533
2596
  const globalRule = ruleset.rules[ruleName];
2534
2597
  const mergedRule = globalRule == null ? convertedRule : mergeRule(globalRule, convertedRule);
2535
- ruleLog('↑ childNodeRule (%s): %O', ruleName, mergedRule);
2598
+ if (ruleEnabled) {
2599
+ ruleLog('↑ childNodeRule (%s): %O', ruleName, mergedRule);
2600
+ }
2536
2601
  for (const descendant of targetDescendants) {
2537
2602
  ruleMapper.set(descendant, ruleName, {
2538
2603
  from: 'childNodeRules',
@@ -43,6 +43,88 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
43
43
  * given to a component as a child is, as it ends up inside what the component renders.
44
44
  */
45
45
  get isForeignElement(): boolean;
46
+ /**
47
+ * Whether two names would be one attribute on this element, for names that no attribute of the
48
+ * element stands behind (the names of a spec, of an option or of a rule value, where the
49
+ * attribute may be missing). It is {@link MLAttr.isNamedBy} without an attribute.
50
+ *
51
+ * The HTML syntax folds the ASCII case. A name that code sets through the DOM API is folded
52
+ * only on an element of the HTML namespace: on an SVG or MathML element it keeps its spelling,
53
+ * and a name with a capital letter is a prop of the framework. The props of a component are
54
+ * not folded, except that Vue camelizes them.
55
+ *
56
+ * With no attribute, whether the name is set through the DOM API is up to the parser alone: the
57
+ * attribute is not dynamic, so Svelte's name is read as the HTML parser reads it.
58
+ *
59
+ * @see https://dom.spec.whatwg.org/#dom-element-setattribute
60
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
61
+ *
62
+ * @param a - A name
63
+ * @param b - The other name
64
+ * @returns `true` if both names are one attribute on this element
65
+ */
66
+ isSameAttrName(a: string, b: string): boolean;
67
+ /**
68
+ * Whether the `#custom` entry of a content model category (`:model(flow)` etc.) covers this element.
69
+ *
70
+ * Anything that is not a native HTML element counts, an authored component (`<Foo />`)
71
+ * as well as an autonomous custom element (`<x-foo>`). Keep it in step with `isTreatedAsCustom`
72
+ * of `permitted-contents` (`'pretended'` mode), which applies the same criterion to `#custom`;
73
+ * they are separate on purpose, as `isTreatedAsCustom` also serves the `'origin'` mode.
74
+ *
75
+ * Like {@link localName}, it follows the pretender, since a selector sees the element
76
+ * the component renders. A pretender target is created with `elementType: 'html'`, so it
77
+ * is a custom element when its own name has a hyphen. A target whose name is a component
78
+ * (`Tabs.Root`; the rule is the parser's, see {@link MLDocument.isComponentName}) is created as
79
+ * `'authored'` and counts like one written directly: the scanners of `pretenders.auto` end a chain at such a name when
80
+ * they cannot resolve it, and what that component renders is not known.
81
+ *
82
+ * Known limitation: the hyphen test is only the hyphen. A pretender target named after a reserved
83
+ * hyphenated name (`annotation-xml`, `font-face`, ...) counts as custom here, whereas the same
84
+ * element written directly does not. `isTreatedAsCustom` shares the limitation.
85
+ */
86
+ get isCustomElement(): boolean;
87
+ /**
88
+ * Whether the document tells the case of a tag name apart (see {@link MLDocument.tagNameCaseSensitive}).
89
+ * A type selector reads it, as {@link localName} alone cannot tell it: an element that HTML names
90
+ * case-insensitively (`<foobar>`) has a lowercase-folded pair of `localName` / `nodeName` in
91
+ * every document, but in JSX it is not `<FooBar>`.
92
+ *
93
+ * @implements `SelectorElement#tagNameCaseSensitive` of `@markuplint/selector`
94
+ */
95
+ get tagNameCaseSensitive(): boolean;
96
+ /**
97
+ * What this element is for a type selector, in a document that resolves a component by any
98
+ * spelling of its name (Vue, see {@link MLDocument.componentNameVariants}); `undefined` in any other.
99
+ *
100
+ * - `component`: not an HTML element (`<FooBar>`, `<foo-bar>`), which {@link localName} names as written
101
+ * - `native`: an HTML element that the document takes for native (`<button>`, see {@link MLDocument.isNativeTag}:
102
+ * the parser's rule, as Vue's `isNativeTag`, else the specification), the guard of {@link pretending}.
103
+ * The element that a pretender stands for is judged by the specification alone
104
+ * - `unknown-html`: an HTML element that it does not (`<foobar>`; in Vue also `<marquee>`, which has a
105
+ * specification but resolves to a component when one is imported)
106
+ *
107
+ * Like {@link localName}, it follows the pretender, since a selector sees the element the component
108
+ * renders. An element of the SVG or MathML namespace has none: it keeps the case of its name.
109
+ *
110
+ * @implements `SelectorElement#tagNameKind` of `@markuplint/selector`
111
+ */
112
+ get tagNameKind(): 'component' | 'native' | 'unknown-html' | undefined;
113
+ /**
114
+ * Whether a name that a user writes for an element (the `tag` of a rule option, as against the
115
+ * type name of a selector, which {@link matches} takes) stands for this element, written as it is,
116
+ * in a document that resolves a component by any spelling of its name (Vue, see
117
+ * {@link MLDocument.componentNameVariants}): `FooBar` stands for `<foo-bar>` and `<fooBar>`.
118
+ *
119
+ * It is `false` in any other document, so that a caller that compares a name as it is can ask
120
+ * it as well without changing what it does there. It ignores the pretender: the name is the one
121
+ * the element is written with. To ask so, it hides the pretender of this element for the length of the
122
+ * call (as {@link matchMLSelector} does for its second try), so nothing in it may be async.
123
+ *
124
+ * @param name - The name as the user wrote it
125
+ * @returns Whether the name stands for this element
126
+ */
127
+ matchesTagNameVariant(name: string): boolean;
46
128
  /**
47
129
  * Whether this element was implicitly created (e.g., an omitted `<body>` tag in HTML).
48
130
  */
@@ -534,6 +616,11 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
534
616
  */
535
617
  get attributeStyleMap(): StylePropertyMap;
536
618
  /**
619
+ * Every attribute written on the tag, the second of two spellings of one name too: unlike a DOM
620
+ * element, which keeps one of them, a rule checks each attribute that the author wrote, and
621
+ * `no-duplicate-attr` reports the second. Which of them the DOM keeps is what {@link getAttribute}
622
+ * returns.
623
+ *
537
624
  * @implements DOM API: `Element`
538
625
  * @see https://dom.spec.whatwg.org/#dom-element-attributes
539
626
  */
@@ -1890,6 +1977,10 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
1890
1977
  */
1891
1978
  getAnimations(options?: GetAnimationsOptions): Animation[];
1892
1979
  /**
1980
+ * Whether `attrName` is the name of an attribute of this element is what {@link MLAttr.isNamedBy}
1981
+ * says: `ROLE` is `role` on a `<div>` and, in JSX, is not on an `<svg>`. `hasAttribute`,
1982
+ * `getAttributeToken`, `getAttributePretended`, a pretender's `fromAttr` and `getNamedItem` ask the same.
1983
+ *
1893
1984
  * @implements DOM API: `Element`
1894
1985
  * @see https://dom.spec.whatwg.org/#ref-for-dom-element-getattribute%E2%91%A0
1895
1986
  */
@@ -1929,7 +2020,7 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
1929
2020
  * the original component element, not on the virtual pretender element.
1930
2021
  *
1931
2022
  * @implements `@markuplint/ml-core` API: `MLElement`
1932
- * @param attrName - The attribute name to look up (case-insensitive)
2023
+ * @param attrName - The attribute name to look up (see {@link MLAttr.isNamedBy} for how its case is compared)
1933
2024
  * @returns The attribute value, or null if the attribute is not found
1934
2025
  */
1935
2026
  getAttributePretended(attrName: string): string | null;
@@ -1938,7 +2029,7 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
1938
2029
  * Unlike `getAttribute`, this returns the full `MLAttr` token objects.
1939
2030
  *
1940
2031
  * @implements `@markuplint/ml-core` API: `MLElement`
1941
- * @param attrName - The attribute name to look up (case-insensitive)
2032
+ * @param attrName - The attribute name to look up (see {@link MLAttr.isNamedBy} for how its case is compared)
1942
2033
  * @returns An array of matching attribute tokens
1943
2034
  */
1944
2035
  getAttributeToken(attrName: string): MLAttr<T, O>[];