@markuplint/ml-core 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 +34 -0
- package/CLAUDE.md +11 -0
- package/lib/debug.d.ts +6 -0
- package/lib/debug.js +6 -0
- package/lib/ml-core.js +55 -16
- package/lib/ml-dom/helper/accname.d.ts +25 -1
- package/lib/ml-dom/helper/accname.js +85 -2
- package/lib/ml-dom/helper/foreign-attr-names.d.ts +13 -0
- package/lib/ml-dom/helper/foreign-attr-names.js +89 -0
- package/lib/ml-dom/node/attr.d.ts +77 -0
- package/lib/ml-dom/node/attr.js +142 -8
- package/lib/ml-dom/node/document.d.ts +53 -1
- package/lib/ml-dom/node/document.js +73 -8
- package/lib/ml-dom/node/element.d.ts +132 -3
- package/lib/ml-dom/node/element.js +274 -36
- package/lib/ml-dom/node/named-node-map.js +1 -1
- package/lib/ml-dom/node/node-store.js +4 -2
- package/lib/ml-dom/node/rule-mapper.js +21 -6
- package/lib/test/index.js +15 -6
- package/package.json +14 -14
package/lib/ml-dom/node/attr.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { resolveNamespace, compileDirectivePatterns, resolveDirective } from '@markuplint/ml-spec';
|
|
2
|
-
import { searchIDLAttribute } from '@markuplint/parser-utils';
|
|
2
|
+
import { searchIDLAttribute, suggestIDLAttribute } from '@markuplint/parser-utils';
|
|
3
|
+
import { adjustForeignAttrName } from '../helper/foreign-attr-names.js';
|
|
3
4
|
import { MLToken } from '../token/token.js';
|
|
4
5
|
import { MLDomTokenList } from './dom-token-list.js';
|
|
5
6
|
import { MLNode } from './node.js';
|
|
6
|
-
import { UnexpectedCallError } from '@markuplint/shared';
|
|
7
|
+
import { UnexpectedCallError, asciiLowerCase, vuePropKey } from '@markuplint/shared';
|
|
7
8
|
/**
|
|
8
9
|
* Represents a DOM Attr (attribute) node wrapper in the markuplint DOM tree.
|
|
9
10
|
* Wraps an AST attribute token and provides access to the attribute's name, value,
|
|
@@ -141,10 +142,12 @@ export class MLAttr extends MLNode {
|
|
|
141
142
|
}
|
|
142
143
|
// Set candidate for IDL naming suggestions (e.g., tabindex → tabIndex in JSX).
|
|
143
144
|
// Only in 'idl' mode (React). In 'both' mode (Svelte), both content and IDL names are accepted.
|
|
144
|
-
if (!resolution &&
|
|
145
|
+
if (!resolution && ownElement.ownerMLDocument.specs.acceptedAttrNames === 'idl') {
|
|
146
|
+
// The property of a spelling that is not its own (`TABINDEX`) is suggested as well
|
|
147
|
+
const suggested = idlPropName ?? suggestIDLAttribute(this.#potentialName);
|
|
145
148
|
const rawName = this.nameNode?.raw ?? '';
|
|
146
|
-
if (rawName !==
|
|
147
|
-
this.candidate =
|
|
149
|
+
if (suggested && rawName !== suggested) {
|
|
150
|
+
this.candidate = suggested;
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
}
|
|
@@ -156,13 +159,17 @@ export class MLAttr extends MLNode {
|
|
|
156
159
|
this.isDirective = this._astToken.isDirective;
|
|
157
160
|
this.isDuplicatable = this._astToken.isDuplicatable;
|
|
158
161
|
}
|
|
159
|
-
|
|
162
|
+
// The HTML parser lower-cases the name before it adjusts a foreign attribute, so `XLINK:HREF`
|
|
163
|
+
// is `xlink:href` there; a name that code sets keeps the case of its prefix.
|
|
164
|
+
const prefixed = this.#potentialName.includes(':') && !this.isSetThroughDOMAPI;
|
|
165
|
+
const nameWithPrefix = prefixed ? asciiLowerCase(this.#potentialName) : this.#potentialName;
|
|
166
|
+
const ns = resolveNamespace(nameWithPrefix, ownElement.namespaceURI);
|
|
160
167
|
this.#localName = ns.localName;
|
|
161
168
|
// An attribute does not inherit the namespace of its element, so only a known prefix
|
|
162
169
|
// gives it one. `ns.namespace` equals the prefix exactly when the prefix is a known
|
|
163
170
|
// one; for an unknown prefix it falls back to the element's namespace.
|
|
164
|
-
const hasPrefix = ns.localName !==
|
|
165
|
-
const prefix =
|
|
171
|
+
const hasPrefix = ns.localName !== nameWithPrefix;
|
|
172
|
+
const prefix = nameWithPrefix.split(':')[0];
|
|
166
173
|
this.#namespaceURI = hasPrefix && ns.namespace === prefix ? ns.namespaceURI : null;
|
|
167
174
|
}
|
|
168
175
|
/**
|
|
@@ -183,6 +190,133 @@ export class MLAttr extends MLNode {
|
|
|
183
190
|
get name() {
|
|
184
191
|
return this.#potentialName;
|
|
185
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* The name that the DOM gives this attribute, which two spellings of a name share when they are
|
|
195
|
+
* one attribute (`ID` and `id` on a `<div>`, `VIEWBOX` and `viewbox` on an `<svg>` that the
|
|
196
|
+
* HTML parser reads). `name` keeps the spelling of the source for a message, and for
|
|
197
|
+
* `case-sensitive-attr-name`.
|
|
198
|
+
*
|
|
199
|
+
* It depends on how the name gets into the DOM (`MLDocument#attrNameRoute`) and on the element:
|
|
200
|
+
* the HTML parser lower-cases a name, and restores the case of the names of SVG and MathML;
|
|
201
|
+
* `setAttribute` lower-cases a name only on an element of the HTML namespace, so `ROLE` on an
|
|
202
|
+
* `<svg>` that code sets is `ROLE`, which is not `role`. The attribute of a component is a prop,
|
|
203
|
+
* which keeps its spelling, except in Vue where `my-label` and `myLabel` are one prop.
|
|
204
|
+
*
|
|
205
|
+
* @see https://dom.spec.whatwg.org/#dom-element-setattribute
|
|
206
|
+
* @see https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
|
|
207
|
+
*/
|
|
208
|
+
get nameKey() {
|
|
209
|
+
return this.keyOf(this.#potentialName);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The key of `name`, a name of a spec, of an option or of a lookup, as this attribute is keyed:
|
|
213
|
+
* `name` is this attribute when its key is {@link MLAttr.nameKey}. Only the ASCII letters fold,
|
|
214
|
+
* as the HTML syntax says (`toLowerCase` also turns U+212A KELVIN SIGN into `k`).
|
|
215
|
+
*
|
|
216
|
+
* It is computed when asked, not stored by the constructor: it reads the owner element (its
|
|
217
|
+
* type and namespace) and the document, which are complete only after the attribute is built.
|
|
218
|
+
*
|
|
219
|
+
* @param name - A name as written
|
|
220
|
+
* @returns The key that two spellings of one attribute share
|
|
221
|
+
*/
|
|
222
|
+
keyOf(name) {
|
|
223
|
+
const el = this.ownerElement;
|
|
224
|
+
if (el.elementType === 'authored') {
|
|
225
|
+
return el.ownerMLDocument.componentNameVariants ? vuePropKey(name) : name;
|
|
226
|
+
}
|
|
227
|
+
if (!el.isForeignElement) {
|
|
228
|
+
return asciiLowerCase(name);
|
|
229
|
+
}
|
|
230
|
+
if (this.isSetThroughDOMAPI) {
|
|
231
|
+
return name;
|
|
232
|
+
}
|
|
233
|
+
return adjustForeignAttrName(asciiLowerCase(name), el.namespaceURI);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Whether `name` is the name of this attribute: the spelling it is written in, or another that
|
|
237
|
+
* the DOM makes the same attribute ({@link MLAttr.keyOf}). A spec, an option and the lookups of
|
|
238
|
+
* the element ask this, so the case of a name is decided here and not in each of them.
|
|
239
|
+
*
|
|
240
|
+
* A name with a capital letter that code sets on an element of the HTML namespace is a prop of
|
|
241
|
+
* the framework (`defaultValue` of React), not an attribute of the DOM: only its exact spelling
|
|
242
|
+
* is it, and `defaultvalue` is not (see {@link MLAttr.isPrefixedBy} for a prefix).
|
|
243
|
+
*
|
|
244
|
+
* @param name - A name of a spec, an option or a lookup
|
|
245
|
+
* @returns `true` if `name` is this attribute
|
|
246
|
+
*/
|
|
247
|
+
isNamedBy(name) {
|
|
248
|
+
if (name === this.#potentialName) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
if (this.#isFrameworkPropName(name)) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
return this.keyOf(name) === this.nameKey;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Whether the name of this attribute starts with `prefix`, a prefix of an option
|
|
258
|
+
* (`ignoreAttrNamePrefix`): as it is written, or as the DOM gives the name ({@link MLAttr.keyOf}).
|
|
259
|
+
* A prefix with a capital letter that code sets on an element of the HTML namespace is the prefix
|
|
260
|
+
* of a name of the framework, and only its exact spelling is it, as for {@link MLAttr.isNamedBy}.
|
|
261
|
+
*
|
|
262
|
+
* @param prefix - A prefix of an option
|
|
263
|
+
* @returns `true` if the name of this attribute starts with `prefix`
|
|
264
|
+
*/
|
|
265
|
+
isPrefixedBy(prefix) {
|
|
266
|
+
if (this.#potentialName.startsWith(prefix)) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
if (this.#isFrameworkPropName(prefix)) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
return this.nameKey.startsWith(this.keyOf(prefix));
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* A name with a capital letter that code sets on an element of the HTML namespace is a prop of the
|
|
276
|
+
* framework (`defaultValue` of React), not an attribute of the DOM: the DOM folds what `setAttribute`
|
|
277
|
+
* is given, and a prop is read as it is written.
|
|
278
|
+
*/
|
|
279
|
+
#isFrameworkPropName(name) {
|
|
280
|
+
const el = this.ownerElement;
|
|
281
|
+
return el.elementType !== 'authored' && !el.isForeignElement && this.isSetThroughDOMAPI && /[A-Z]/.test(name);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Whether the name of this attribute reaches the DOM through the DOM API (`setAttribute`) and
|
|
285
|
+
* not through the HTML parser of the browser: what the parser says of its language
|
|
286
|
+
* (`MLDocument#attrNameRoute`), and for a parser that says it of a dynamic attribute only
|
|
287
|
+
* (Svelte), whether this one is dynamic.
|
|
288
|
+
*/
|
|
289
|
+
get isSetThroughDOMAPI() {
|
|
290
|
+
const route = this.ownerElement.ownerMLDocument.attrNameRoute;
|
|
291
|
+
return route === 'dom-api' || (route === 'dom-api-when-dynamic' && this.isDynamicValue === true);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Whether a selector matches the name of this attribute without regard to ASCII case: the DOM
|
|
295
|
+
* does so for an element of the HTML namespace and not for one of SVG or MathML, nor for a prop.
|
|
296
|
+
*
|
|
297
|
+
* @implements `SelectorAttr#nameCaseInsensitive` of `@markuplint/selector`
|
|
298
|
+
*/
|
|
299
|
+
get nameCaseInsensitive() {
|
|
300
|
+
const el = this.ownerElement;
|
|
301
|
+
return el.elementType !== 'authored' && !el.isForeignElement;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The name that a selector is matched with.
|
|
305
|
+
*
|
|
306
|
+
* @implements `SelectorAttr#domName` of `@markuplint/selector`
|
|
307
|
+
*/
|
|
308
|
+
get domName() {
|
|
309
|
+
return this.nameKey;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Whether a name of a selector is this prop once it is camelized: the props of a Vue component.
|
|
313
|
+
*
|
|
314
|
+
* @implements `SelectorAttr#nameIsCamelized` of `@markuplint/selector`
|
|
315
|
+
*/
|
|
316
|
+
get nameIsCamelized() {
|
|
317
|
+
const el = this.ownerElement;
|
|
318
|
+
return el.elementType === 'authored' && el.ownerMLDocument.componentNameVariants;
|
|
319
|
+
}
|
|
186
320
|
/**
|
|
187
321
|
* Returns the namespace URI of this attribute: that of its prefix when the name has a known
|
|
188
322
|
* one (e.g. `xlink:href`), otherwise `null`. An attribute without a prefix has no namespace
|
|
@@ -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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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',
|
|
@@ -5,7 +5,7 @@ import type { MLText } from './text.js';
|
|
|
5
5
|
import type { ElementNodeType, PretenderContext } from './types.js';
|
|
6
6
|
import type { ElementType, MLASTBlockBehavior, MLASTElement, NamespaceURI } from '@markuplint/ml-ast';
|
|
7
7
|
import type { PlainData, Pretender, RegexSelector, RuleConfigValue, RuleInfo } from '@markuplint/ml-config';
|
|
8
|
-
import type { ARIAVersion } from '@markuplint/ml-spec';
|
|
8
|
+
import type { AccnameSource, AccnameVisit, ARIAVersion } from '@markuplint/ml-spec';
|
|
9
9
|
import type { SelectorMatches } from '@markuplint/selector';
|
|
10
10
|
import { MLAttr } from './attr.js';
|
|
11
11
|
import { MLDomTokenList } from './dom-token-list.js';
|
|
@@ -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
|
*/
|
|
@@ -1881,6 +1968,44 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
|
|
|
1881
1968
|
* @returns The computed accessible name string (may be empty)
|
|
1882
1969
|
*/
|
|
1883
1970
|
getAccessibleName(version: ARIAVersion): string;
|
|
1971
|
+
/**
|
|
1972
|
+
* Returns the elements that the computation of this element's text
|
|
1973
|
+
* alternative reaches, with how it reaches each, memoized per ARIA version
|
|
1974
|
+
* and mode like {@link getAccessibleName}.
|
|
1975
|
+
*
|
|
1976
|
+
* A descendant missing from the map takes no part in the text alternative:
|
|
1977
|
+
* the computation ends before it (a hidden element, a name given before
|
|
1978
|
+
* content, a component that does not render its children). When the
|
|
1979
|
+
* computation fails, the map is empty.
|
|
1980
|
+
*
|
|
1981
|
+
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
1982
|
+
* @param version - The ARIA specification version to use for computation
|
|
1983
|
+
* @param asReferenced - Computes it as a node referenced by `aria-labelledby`
|
|
1984
|
+
* or `aria-describedby` instead of as this element's accessible name
|
|
1985
|
+
* @returns The reached elements and their visits
|
|
1986
|
+
*/
|
|
1987
|
+
getAccessibleNameVisits(version: ARIAVersion, asReferenced?: boolean): ReadonlyMap<MLElement<any, any>, readonly AccnameVisit[]>;
|
|
1988
|
+
/**
|
|
1989
|
+
* Returns the text alternative of this element as a node referenced by
|
|
1990
|
+
* `aria-labelledby` or `aria-describedby` (AccName Step 2B, LabelledBy
|
|
1991
|
+
* Traversal), memoized per ARIA version like {@link getAccessibleName}.
|
|
1992
|
+
*
|
|
1993
|
+
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
1994
|
+
* @param version - The ARIA specification version to use for computation
|
|
1995
|
+
* @returns The text alternative (may be empty)
|
|
1996
|
+
*/
|
|
1997
|
+
getReferencedTextAlternative(version: ARIAVersion): string;
|
|
1998
|
+
/**
|
|
1999
|
+
* Returns the source of this element's host language text alternative
|
|
2000
|
+
* (AccName Step 2E), such as `'alt'` or `'svg-title'`, whether or not an
|
|
2001
|
+
* explicit `none` or `presentation` role keeps it out of the name.
|
|
2002
|
+
* Memoized per ARIA version like {@link getAccessibleName}.
|
|
2003
|
+
*
|
|
2004
|
+
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
2005
|
+
* @param version - The ARIA specification version to use for computation
|
|
2006
|
+
* @returns The source, or `null` if the element has no host language text alternative
|
|
2007
|
+
*/
|
|
2008
|
+
getHostLanguageLabelSource(version: ARIAVersion): AccnameSource | null;
|
|
1884
2009
|
/**
|
|
1885
2010
|
* **IT THROWS AN ERROR WHEN CALLING THIS.**
|
|
1886
2011
|
*
|
|
@@ -1890,6 +2015,10 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
|
|
|
1890
2015
|
*/
|
|
1891
2016
|
getAnimations(options?: GetAnimationsOptions): Animation[];
|
|
1892
2017
|
/**
|
|
2018
|
+
* Whether `attrName` is the name of an attribute of this element is what {@link MLAttr.isNamedBy}
|
|
2019
|
+
* says: `ROLE` is `role` on a `<div>` and, in JSX, is not on an `<svg>`. `hasAttribute`,
|
|
2020
|
+
* `getAttributeToken`, `getAttributePretended`, a pretender's `fromAttr` and `getNamedItem` ask the same.
|
|
2021
|
+
*
|
|
1893
2022
|
* @implements DOM API: `Element`
|
|
1894
2023
|
* @see https://dom.spec.whatwg.org/#ref-for-dom-element-getattribute%E2%91%A0
|
|
1895
2024
|
*/
|
|
@@ -1929,7 +2058,7 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
|
|
|
1929
2058
|
* the original component element, not on the virtual pretender element.
|
|
1930
2059
|
*
|
|
1931
2060
|
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
1932
|
-
* @param attrName - The attribute name to look up (case
|
|
2061
|
+
* @param attrName - The attribute name to look up (see {@link MLAttr.isNamedBy} for how its case is compared)
|
|
1933
2062
|
* @returns The attribute value, or null if the attribute is not found
|
|
1934
2063
|
*/
|
|
1935
2064
|
getAttributePretended(attrName: string): string | null;
|
|
@@ -1938,7 +2067,7 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
|
|
|
1938
2067
|
* Unlike `getAttribute`, this returns the full `MLAttr` token objects.
|
|
1939
2068
|
*
|
|
1940
2069
|
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
1941
|
-
* @param attrName - The attribute name to look up (case
|
|
2070
|
+
* @param attrName - The attribute name to look up (see {@link MLAttr.isNamedBy} for how its case is compared)
|
|
1942
2071
|
* @returns An array of matching attribute tokens
|
|
1943
2072
|
*/
|
|
1944
2073
|
getAttributeToken(attrName: string): MLAttr<T, O>[];
|