@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
|
@@ -3,8 +3,8 @@ var _a;
|
|
|
3
3
|
import { getSpecByTagName, resolveNamespace } from '@markuplint/ml-spec';
|
|
4
4
|
import { pretenderNamespaceToURI } from '@markuplint/ml-config';
|
|
5
5
|
import { getChildNamespace } from '@markuplint/parser-utils';
|
|
6
|
-
import { matchSelector } from '@markuplint/selector';
|
|
7
|
-
import { getAccname } from '../helper/accname.js';
|
|
6
|
+
import { matchSelector, typeNameMatches } from '@markuplint/selector';
|
|
7
|
+
import { getAccname, getAccnameVisits, getHostLanguageLabelSource, getReferencedTextAlternative, } from '../helper/accname.js';
|
|
8
8
|
import { after, before, nextElementSibling, previousElementSibling, remove, replaceWith, } from '../manipulations/child-node-methods.js';
|
|
9
9
|
import { MLAttr } from './attr.js';
|
|
10
10
|
import { MLDomTokenList } from './dom-token-list.js';
|
|
@@ -12,7 +12,7 @@ import { MLElementCloseTag } from './element-close-tag.js';
|
|
|
12
12
|
import { toNamedNodeMap } from './named-node-map.js';
|
|
13
13
|
import { toHTMLCollection, toNodeList } from './node-list.js';
|
|
14
14
|
import { MLParentNode } from './parent-node.js';
|
|
15
|
-
import { UnexpectedCallError } from '@markuplint/shared';
|
|
15
|
+
import { UnexpectedCallError, asciiLowerCase, vuePropKey } from '@markuplint/shared';
|
|
16
16
|
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
17
17
|
/**
|
|
18
18
|
* Represents a DOM Element node wrapper in the markuplint DOM tree.
|
|
@@ -49,6 +49,140 @@ export class MLElement extends MLParentNode {
|
|
|
49
49
|
get isForeignElement() {
|
|
50
50
|
return this.#namespaceURI !== HTML_NAMESPACE;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether two names would be one attribute on this element, for names that no attribute of the
|
|
54
|
+
* element stands behind (the names of a spec, of an option or of a rule value, where the
|
|
55
|
+
* attribute may be missing). It is {@link MLAttr.isNamedBy} without an attribute.
|
|
56
|
+
*
|
|
57
|
+
* The HTML syntax folds the ASCII case. A name that code sets through the DOM API is folded
|
|
58
|
+
* only on an element of the HTML namespace: on an SVG or MathML element it keeps its spelling,
|
|
59
|
+
* and a name with a capital letter is a prop of the framework. The props of a component are
|
|
60
|
+
* not folded, except that Vue camelizes them.
|
|
61
|
+
*
|
|
62
|
+
* With no attribute, whether the name is set through the DOM API is up to the parser alone: the
|
|
63
|
+
* attribute is not dynamic, so Svelte's name is read as the HTML parser reads it.
|
|
64
|
+
*
|
|
65
|
+
* @see https://dom.spec.whatwg.org/#dom-element-setattribute
|
|
66
|
+
* @see https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
|
|
67
|
+
*
|
|
68
|
+
* @param a - A name
|
|
69
|
+
* @param b - The other name
|
|
70
|
+
* @returns `true` if both names are one attribute on this element
|
|
71
|
+
*/
|
|
72
|
+
isSameAttrName(a, b) {
|
|
73
|
+
if (a === b) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (this.elementType === 'authored') {
|
|
77
|
+
return this.ownerMLDocument.componentNameVariants && vuePropKey(a) === vuePropKey(b);
|
|
78
|
+
}
|
|
79
|
+
if (this.ownerMLDocument.attrNameRoute === 'dom-api' && (this.isForeignElement || /[A-Z]/.test(a + b))) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return asciiLowerCase(a) === asciiLowerCase(b);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether the `#custom` entry of a content model category (`:model(flow)` etc.) covers this element.
|
|
86
|
+
*
|
|
87
|
+
* Anything that is not a native HTML element counts, an authored component (`<Foo />`)
|
|
88
|
+
* as well as an autonomous custom element (`<x-foo>`). Keep it in step with `isTreatedAsCustom`
|
|
89
|
+
* of `permitted-contents` (`'pretended'` mode), which applies the same criterion to `#custom`;
|
|
90
|
+
* they are separate on purpose, as `isTreatedAsCustom` also serves the `'origin'` mode.
|
|
91
|
+
*
|
|
92
|
+
* Like {@link localName}, it follows the pretender, since a selector sees the element
|
|
93
|
+
* the component renders. A pretender target is created with `elementType: 'html'`, so it
|
|
94
|
+
* is a custom element when its own name has a hyphen. A target whose name is a component
|
|
95
|
+
* (`Tabs.Root`; the rule is the parser's, see {@link MLDocument.isComponentName}) is created as
|
|
96
|
+
* `'authored'` and counts like one written directly: the scanners of `pretenders.auto` end a chain at such a name when
|
|
97
|
+
* they cannot resolve it, and what that component renders is not known.
|
|
98
|
+
*
|
|
99
|
+
* Known limitation: the hyphen test is only the hyphen. A pretender target named after a reserved
|
|
100
|
+
* hyphenated name (`annotation-xml`, `font-face`, ...) counts as custom here, whereas the same
|
|
101
|
+
* element written directly does not. `isTreatedAsCustom` shares the limitation.
|
|
102
|
+
*/
|
|
103
|
+
get isCustomElement() {
|
|
104
|
+
if (this.pretenderContext?.type === 'pretender') {
|
|
105
|
+
const { as } = this.pretenderContext;
|
|
106
|
+
return as.elementType === 'authored' || as.localName.includes('-');
|
|
107
|
+
}
|
|
108
|
+
return this.elementType !== 'html';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Whether the document tells the case of a tag name apart (see {@link MLDocument.tagNameCaseSensitive}).
|
|
112
|
+
* A type selector reads it, as {@link localName} alone cannot tell it: an element that HTML names
|
|
113
|
+
* case-insensitively (`<foobar>`) has a lowercase-folded pair of `localName` / `nodeName` in
|
|
114
|
+
* every document, but in JSX it is not `<FooBar>`.
|
|
115
|
+
*
|
|
116
|
+
* @implements `SelectorElement#tagNameCaseSensitive` of `@markuplint/selector`
|
|
117
|
+
*/
|
|
118
|
+
get tagNameCaseSensitive() {
|
|
119
|
+
return this.ownerMLDocument.tagNameCaseSensitive;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* What this element is for a type selector, in a document that resolves a component by any
|
|
123
|
+
* spelling of its name (Vue, see {@link MLDocument.componentNameVariants}); `undefined` in any other.
|
|
124
|
+
*
|
|
125
|
+
* - `component`: not an HTML element (`<FooBar>`, `<foo-bar>`), which {@link localName} names as written
|
|
126
|
+
* - `native`: an HTML element that the document takes for native (`<button>`, see {@link MLDocument.isNativeTag}:
|
|
127
|
+
* the parser's rule, as Vue's `isNativeTag`, else the specification), the guard of {@link pretending}.
|
|
128
|
+
* The element that a pretender stands for is judged by the specification alone
|
|
129
|
+
* - `unknown-html`: an HTML element that it does not (`<foobar>`; in Vue also `<marquee>`, which has a
|
|
130
|
+
* specification but resolves to a component when one is imported)
|
|
131
|
+
*
|
|
132
|
+
* Like {@link localName}, it follows the pretender, since a selector sees the element the component
|
|
133
|
+
* renders. An element of the SVG or MathML namespace has none: it keeps the case of its name.
|
|
134
|
+
*
|
|
135
|
+
* @implements `SelectorElement#tagNameKind` of `@markuplint/selector`
|
|
136
|
+
*/
|
|
137
|
+
get tagNameKind() {
|
|
138
|
+
const document = this.ownerMLDocument;
|
|
139
|
+
if (!document.componentNameVariants) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const target = this.pretenderContext?.type === 'pretender' ? this.pretenderContext.as : this;
|
|
143
|
+
if (target.isForeignElement) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (target.elementType !== 'html') {
|
|
147
|
+
return 'component';
|
|
148
|
+
}
|
|
149
|
+
// A tag as written is the language's to resolve; the name a pretender gives (`as`) is the element itself
|
|
150
|
+
const isNative = target === this
|
|
151
|
+
? document.isNativeTag(target.localName, target.namespaceURI)
|
|
152
|
+
: getSpecByTagName(document.specs.specs, target.localName, target.namespaceURI) != null;
|
|
153
|
+
return isNative ? 'native' : 'unknown-html';
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether a name that a user writes for an element (the `tag` of a rule option, as against the
|
|
157
|
+
* type name of a selector, which {@link matches} takes) stands for this element, written as it is,
|
|
158
|
+
* in a document that resolves a component by any spelling of its name (Vue, see
|
|
159
|
+
* {@link MLDocument.componentNameVariants}): `FooBar` stands for `<foo-bar>` and `<fooBar>`.
|
|
160
|
+
*
|
|
161
|
+
* It is `false` in any other document, so that a caller that compares a name as it is can ask
|
|
162
|
+
* it as well without changing what it does there. It ignores the pretender: the name is the one
|
|
163
|
+
* the element is written with. To ask so, it hides the pretender of this element for the length of the
|
|
164
|
+
* call (as {@link matchMLSelector} does for its second try), so nothing in it may be async.
|
|
165
|
+
*
|
|
166
|
+
* @param name - The name as the user wrote it
|
|
167
|
+
* @returns Whether the name stands for this element
|
|
168
|
+
*/
|
|
169
|
+
matchesTagNameVariant(name) {
|
|
170
|
+
if (!this.ownerMLDocument.componentNameVariants) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
const pretenderContext = this.pretenderContext;
|
|
174
|
+
if (pretenderContext?.type !== 'pretender') {
|
|
175
|
+
return this.tagNameKind !== undefined && typeNameMatches(name, this);
|
|
176
|
+
}
|
|
177
|
+
// The element as written, not as the pretender makes it: as `matchMLSelector` does for its second try
|
|
178
|
+
this.pretenderContext = null;
|
|
179
|
+
try {
|
|
180
|
+
return this.tagNameKind !== undefined && typeNameMatches(name, this);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
this.pretenderContext = pretenderContext;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
52
186
|
/**
|
|
53
187
|
* Whether this element was implicitly created (e.g., an omitted `<body>` tag in HTML).
|
|
54
188
|
*/
|
|
@@ -93,6 +227,21 @@ export class MLElement extends MLParentNode {
|
|
|
93
227
|
* @see {@link getAccessibleName} — the public method that reads/writes this cache
|
|
94
228
|
*/
|
|
95
229
|
#accessibleNameCache = new Map();
|
|
230
|
+
/**
|
|
231
|
+
* Memoizes {@link getAccessibleNameVisits}, keyed by `${version}:${asReferenced}`.
|
|
232
|
+
* Safe for the same reason as {@link #accessibleNameCache}.
|
|
233
|
+
*/
|
|
234
|
+
#accessibleNameVisitsCache = new Map();
|
|
235
|
+
/**
|
|
236
|
+
* Memoizes {@link getHostLanguageLabelSource}. Safe for the same reason as
|
|
237
|
+
* {@link #accessibleNameCache}.
|
|
238
|
+
*/
|
|
239
|
+
#hostLanguageLabelSourceCache = new Map();
|
|
240
|
+
/**
|
|
241
|
+
* Memoizes {@link getReferencedTextAlternative}. Safe for the same reason as
|
|
242
|
+
* {@link #accessibleNameCache}.
|
|
243
|
+
*/
|
|
244
|
+
#referencedTextAlternativeCache = new Map();
|
|
96
245
|
#normalizedAttrs = new Map();
|
|
97
246
|
#normalizedString = null;
|
|
98
247
|
ontouchcancel;
|
|
@@ -700,28 +849,21 @@ export class MLElement extends MLParentNode {
|
|
|
700
849
|
throw new UnexpectedCallError('Not supported "attributeStyleMap" property');
|
|
701
850
|
}
|
|
702
851
|
/**
|
|
852
|
+
* Every attribute written on the tag, the second of two spellings of one name too: unlike a DOM
|
|
853
|
+
* element, which keeps one of them, a rule checks each attribute that the author wrote, and
|
|
854
|
+
* `no-duplicate-attr` reports the second. Which of them the DOM keeps is what {@link getAttribute}
|
|
855
|
+
* returns.
|
|
856
|
+
*
|
|
703
857
|
* @implements DOM API: `Element`
|
|
704
858
|
* @see https://dom.spec.whatwg.org/#dom-element-attributes
|
|
705
859
|
*/
|
|
706
860
|
get attributes() {
|
|
707
861
|
const origin = this.pretenderContext?.type === 'pretender' ? this.pretenderContext.as.#attributes : this.#attributes;
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
const names = new Set();
|
|
712
|
-
const attrs = [];
|
|
713
|
-
for (const attr of origin) {
|
|
714
|
-
if (names.has(attr.name)) {
|
|
715
|
-
/**
|
|
716
|
-
* Skips a duplicated attribute
|
|
717
|
-
*
|
|
718
|
-
*@see https://html.spec.whatwg.org/#parse-error-duplicate-attribute
|
|
719
|
-
*/
|
|
720
|
-
continue;
|
|
721
|
-
}
|
|
722
|
-
attrs.push(attr);
|
|
862
|
+
const cached = this.#normalizedAttrs.get(origin);
|
|
863
|
+
if (cached) {
|
|
864
|
+
return cached;
|
|
723
865
|
}
|
|
724
|
-
const map = toNamedNodeMap(
|
|
866
|
+
const map = toNamedNodeMap(origin);
|
|
725
867
|
this.#normalizedAttrs.set(origin, map);
|
|
726
868
|
return map;
|
|
727
869
|
}
|
|
@@ -2462,6 +2604,68 @@ export class MLElement extends MLParentNode {
|
|
|
2462
2604
|
this.#accessibleNameCache.set(version, name);
|
|
2463
2605
|
return name;
|
|
2464
2606
|
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Returns the elements that the computation of this element's text
|
|
2609
|
+
* alternative reaches, with how it reaches each, memoized per ARIA version
|
|
2610
|
+
* and mode like {@link getAccessibleName}.
|
|
2611
|
+
*
|
|
2612
|
+
* A descendant missing from the map takes no part in the text alternative:
|
|
2613
|
+
* the computation ends before it (a hidden element, a name given before
|
|
2614
|
+
* content, a component that does not render its children). When the
|
|
2615
|
+
* computation fails, the map is empty.
|
|
2616
|
+
*
|
|
2617
|
+
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
2618
|
+
* @param version - The ARIA specification version to use for computation
|
|
2619
|
+
* @param asReferenced - Computes it as a node referenced by `aria-labelledby`
|
|
2620
|
+
* or `aria-describedby` instead of as this element's accessible name
|
|
2621
|
+
* @returns The reached elements and their visits
|
|
2622
|
+
*/
|
|
2623
|
+
getAccessibleNameVisits(version, asReferenced = false) {
|
|
2624
|
+
const key = `${version}:${asReferenced}`;
|
|
2625
|
+
const cached = this.#accessibleNameVisitsCache.get(key);
|
|
2626
|
+
if (cached != null) {
|
|
2627
|
+
return cached;
|
|
2628
|
+
}
|
|
2629
|
+
const visits = getAccnameVisits(this, version, asReferenced);
|
|
2630
|
+
this.#accessibleNameVisitsCache.set(key, visits);
|
|
2631
|
+
return visits;
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* Returns the text alternative of this element as a node referenced by
|
|
2635
|
+
* `aria-labelledby` or `aria-describedby` (AccName Step 2B, LabelledBy
|
|
2636
|
+
* Traversal), memoized per ARIA version like {@link getAccessibleName}.
|
|
2637
|
+
*
|
|
2638
|
+
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
2639
|
+
* @param version - The ARIA specification version to use for computation
|
|
2640
|
+
* @returns The text alternative (may be empty)
|
|
2641
|
+
*/
|
|
2642
|
+
getReferencedTextAlternative(version) {
|
|
2643
|
+
const cached = this.#referencedTextAlternativeCache.get(version);
|
|
2644
|
+
if (cached != null) {
|
|
2645
|
+
return cached;
|
|
2646
|
+
}
|
|
2647
|
+
const text = getReferencedTextAlternative(this, version);
|
|
2648
|
+
this.#referencedTextAlternativeCache.set(version, text);
|
|
2649
|
+
return text;
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Returns the source of this element's host language text alternative
|
|
2653
|
+
* (AccName Step 2E), such as `'alt'` or `'svg-title'`, whether or not an
|
|
2654
|
+
* explicit `none` or `presentation` role keeps it out of the name.
|
|
2655
|
+
* Memoized per ARIA version like {@link getAccessibleName}.
|
|
2656
|
+
*
|
|
2657
|
+
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
2658
|
+
* @param version - The ARIA specification version to use for computation
|
|
2659
|
+
* @returns The source, or `null` if the element has no host language text alternative
|
|
2660
|
+
*/
|
|
2661
|
+
getHostLanguageLabelSource(version) {
|
|
2662
|
+
if (this.#hostLanguageLabelSourceCache.has(version)) {
|
|
2663
|
+
return this.#hostLanguageLabelSourceCache.get(version) ?? null;
|
|
2664
|
+
}
|
|
2665
|
+
const source = getHostLanguageLabelSource(this, version);
|
|
2666
|
+
this.#hostLanguageLabelSourceCache.set(version, source);
|
|
2667
|
+
return source;
|
|
2668
|
+
}
|
|
2465
2669
|
/**
|
|
2466
2670
|
* **IT THROWS AN ERROR WHEN CALLING THIS.**
|
|
2467
2671
|
*
|
|
@@ -2475,16 +2679,31 @@ export class MLElement extends MLParentNode {
|
|
|
2475
2679
|
throw new UnexpectedCallError('Not supported "getAnimations" method');
|
|
2476
2680
|
}
|
|
2477
2681
|
/**
|
|
2682
|
+
* Whether `attrName` is the name of an attribute of this element is what {@link MLAttr.isNamedBy}
|
|
2683
|
+
* says: `ROLE` is `role` on a `<div>` and, in JSX, is not on an `<svg>`. `hasAttribute`,
|
|
2684
|
+
* `getAttributeToken`, `getAttributePretended`, a pretender's `fromAttr` and `getNamedItem` ask the same.
|
|
2685
|
+
*
|
|
2478
2686
|
* @implements DOM API: `Element`
|
|
2479
2687
|
* @see https://dom.spec.whatwg.org/#ref-for-dom-element-getattribute%E2%91%A0
|
|
2480
2688
|
*/
|
|
2481
2689
|
getAttribute(attrName) {
|
|
2690
|
+
let first = null;
|
|
2691
|
+
let lastSetByCode = null;
|
|
2482
2692
|
for (const attr of this.attributes) {
|
|
2483
|
-
if (attr.
|
|
2484
|
-
|
|
2693
|
+
if (!attr.isNamedBy(attrName)) {
|
|
2694
|
+
continue;
|
|
2695
|
+
}
|
|
2696
|
+
first ??= attr;
|
|
2697
|
+
// A directive, and an attribute that may be written twice (`class` and `:class`), are not
|
|
2698
|
+
// two spellings of one name: the first is the one that is asked for
|
|
2699
|
+
if (attr.isSetThroughDOMAPI && !attr.isDirective && !attr.isDuplicatable) {
|
|
2700
|
+
lastSetByCode = attr;
|
|
2485
2701
|
}
|
|
2486
2702
|
}
|
|
2487
|
-
|
|
2703
|
+
// Of two attributes of one name, the HTML parser keeps the first and drops the second, while code
|
|
2704
|
+
// that sets them one after another (`setAttribute`) lets the second overwrite the first.
|
|
2705
|
+
// @see https://html.spec.whatwg.org/#parse-error-duplicate-attribute
|
|
2706
|
+
return (lastSetByCode ?? first)?.value ?? null;
|
|
2488
2707
|
}
|
|
2489
2708
|
/**
|
|
2490
2709
|
* **IT THROWS AN ERROR WHEN CALLING THIS.**
|
|
@@ -2529,12 +2748,12 @@ export class MLElement extends MLParentNode {
|
|
|
2529
2748
|
* the original component element, not on the virtual pretender element.
|
|
2530
2749
|
*
|
|
2531
2750
|
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
2532
|
-
* @param attrName - The attribute name to look up (case
|
|
2751
|
+
* @param attrName - The attribute name to look up (see {@link MLAttr.isNamedBy} for how its case is compared)
|
|
2533
2752
|
* @returns The attribute value, or null if the attribute is not found
|
|
2534
2753
|
*/
|
|
2535
2754
|
getAttributePretended(attrName) {
|
|
2536
2755
|
for (const attr of this.#attributes) {
|
|
2537
|
-
if (attr.
|
|
2756
|
+
if (attr.isNamedBy(attrName)) {
|
|
2538
2757
|
return attr.value;
|
|
2539
2758
|
}
|
|
2540
2759
|
}
|
|
@@ -2545,14 +2764,13 @@ export class MLElement extends MLParentNode {
|
|
|
2545
2764
|
* Unlike `getAttribute`, this returns the full `MLAttr` token objects.
|
|
2546
2765
|
*
|
|
2547
2766
|
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
2548
|
-
* @param attrName - The attribute name to look up (case
|
|
2767
|
+
* @param attrName - The attribute name to look up (see {@link MLAttr.isNamedBy} for how its case is compared)
|
|
2549
2768
|
* @returns An array of matching attribute tokens
|
|
2550
2769
|
*/
|
|
2551
2770
|
getAttributeToken(attrName) {
|
|
2552
2771
|
const attrs = [];
|
|
2553
|
-
attrName = attrName.toLowerCase();
|
|
2554
2772
|
for (const attr of this.getAttributeTokens()) {
|
|
2555
|
-
if (attr.
|
|
2773
|
+
if (attr.isNamedBy(attrName)) {
|
|
2556
2774
|
attrs.push(attr);
|
|
2557
2775
|
}
|
|
2558
2776
|
}
|
|
@@ -2960,7 +3178,8 @@ export class MLElement extends MLParentNode {
|
|
|
2960
3178
|
}
|
|
2961
3179
|
/**
|
|
2962
3180
|
* The namespace of an element named `name` rendered where this element is, for the
|
|
2963
|
-
* element this element pretends to be.
|
|
3181
|
+
* element this element pretends to be. `null` for what is not an element, a
|
|
3182
|
+
* component.
|
|
2964
3183
|
*
|
|
2965
3184
|
* Where this element is has the namespace of this element, so the element gets the
|
|
2966
3185
|
* namespace an element written there gets. For a component, it is taken from the
|
|
@@ -2990,13 +3209,16 @@ export class MLElement extends MLParentNode {
|
|
|
2990
3209
|
// mask spec-driven rules — deprecation, ARIA role restrictions, browser support —
|
|
2991
3210
|
// keyed on the original tag. See issue #3740.
|
|
2992
3211
|
//
|
|
3212
|
+
// What is recognised is what the document says (`MLDocument#isNativeTag`): the specification, unless
|
|
3213
|
+
// the parser has a rule of its own. Vue's rule does not hold `<marquee>`, which Vue resolves to a
|
|
3214
|
+
// component when one is imported, so there it is pretender-eligible. See issue #4351.
|
|
3215
|
+
//
|
|
2993
3216
|
// Names that the HTML parser cannot distinguish from typos (PascalCase JSX-like
|
|
2994
3217
|
// usage in plain HTML such as `<SimpleButton>`) get `elementType === 'html'`
|
|
2995
3218
|
// from the parser but have no spec entry; those remain pretender-eligible
|
|
2996
3219
|
// (this is what `pretenders.scan` relies on). The legacy "no inline `as=` on
|
|
2997
3220
|
// HTML elements" guard is preserved further down for that case.
|
|
2998
|
-
if (this.elementType === 'html' &&
|
|
2999
|
-
getSpecByTagName(this.ownerMLDocument.specs.specs, this.localName, this.namespaceURI) != null) {
|
|
3221
|
+
if (this.elementType === 'html' && this.ownerMLDocument.isNativeTag(this.localName, this.namespaceURI)) {
|
|
3000
3222
|
return;
|
|
3001
3223
|
}
|
|
3002
3224
|
const pretenderConfig = pretenders?.find(option => this.matches(option.selector));
|
|
@@ -3028,16 +3250,19 @@ export class MLElement extends MLParentNode {
|
|
|
3028
3250
|
aria = pretenderElement.aria;
|
|
3029
3251
|
}
|
|
3030
3252
|
const slots = typeof pretenderElement === 'string' ? undefined : pretenderElement.slots;
|
|
3253
|
+
// A component is given no name for the namespace, as `#resolveNamespace` does:
|
|
3254
|
+
// `getChildNamespace` compares names in lowercase, so `Svg` would enter SVG.
|
|
3255
|
+
const isComponent = this.ownerMLDocument.isComponentName(nodeName);
|
|
3031
3256
|
const namespace = typeof pretenderElement !== 'string' && pretenderElement.namespace
|
|
3032
3257
|
? pretenderNamespaceToURI(pretenderElement.namespace)
|
|
3033
|
-
: this.#getNamespaceInPlace(nodeName, renderedParent);
|
|
3258
|
+
: this.#getNamespaceInPlace(isComponent ? null : nodeName, renderedParent);
|
|
3034
3259
|
const as = new _a({
|
|
3035
3260
|
...this._astToken,
|
|
3036
3261
|
uuid: this.uuid + '_pretender',
|
|
3037
3262
|
raw: `<${nodeName}>`,
|
|
3038
3263
|
nodeName,
|
|
3039
3264
|
namespace,
|
|
3040
|
-
elementType: 'html',
|
|
3265
|
+
elementType: isComponent ? 'authored' : 'html',
|
|
3041
3266
|
attributes,
|
|
3042
3267
|
}, this.ownerMLDocument);
|
|
3043
3268
|
// When slots is null, the component does not accept children (void-like).
|
|
@@ -3081,8 +3306,13 @@ export class MLElement extends MLParentNode {
|
|
|
3081
3306
|
_value = value;
|
|
3082
3307
|
}
|
|
3083
3308
|
else if (value != null && 'fromAttr' in value) {
|
|
3084
|
-
|
|
3085
|
-
|
|
3309
|
+
// The attribute of the usage site that the prop is. Vue gives a prop the attribute by the camelCase
|
|
3310
|
+
// of its name (`my-label` and `myLabel` are the prop `myLabel`, and `mylabel` is another),
|
|
3311
|
+
// and the prop declared as `'my-label'` is the same one, so both sides are read so. Elsewhere
|
|
3312
|
+
// the attribute is read as the DOM or the framework reads it (`isNamedBy`).
|
|
3313
|
+
const { fromAttr } = value;
|
|
3314
|
+
const isVue = this.ownerMLDocument.componentNameVariants;
|
|
3315
|
+
const origin = this.#attributes.find(attr => isVue ? vuePropKey(attr.name) === vuePropKey(fromAttr) : attr.isNamedBy(fromAttr));
|
|
3086
3316
|
if (!origin && value.omitIfMissing) {
|
|
3087
3317
|
return [];
|
|
3088
3318
|
}
|
|
@@ -3157,6 +3387,14 @@ export class MLElement extends MLParentNode {
|
|
|
3157
3387
|
if (spec.attrs) {
|
|
3158
3388
|
attributes.push(...this.#createPretenderAttrs(spec.attrs, uuid));
|
|
3159
3389
|
}
|
|
3390
|
+
const isComponent = this.ownerMLDocument.isComponentName(spec.element);
|
|
3391
|
+
let elementType = 'html';
|
|
3392
|
+
if (isComponent) {
|
|
3393
|
+
elementType = 'authored';
|
|
3394
|
+
}
|
|
3395
|
+
else if (spec.element.includes('-')) {
|
|
3396
|
+
elementType = 'web-component';
|
|
3397
|
+
}
|
|
3160
3398
|
const virtual = new _a({
|
|
3161
3399
|
...this._astToken,
|
|
3162
3400
|
uuid,
|
|
@@ -3164,8 +3402,8 @@ export class MLElement extends MLParentNode {
|
|
|
3164
3402
|
nodeName: spec.element,
|
|
3165
3403
|
namespace: spec.namespace
|
|
3166
3404
|
? pretenderNamespaceToURI(spec.namespace)
|
|
3167
|
-
: renderedIn.#getChildNamespace(spec.element),
|
|
3168
|
-
elementType
|
|
3405
|
+
: renderedIn.#getChildNamespace(isComponent ? null : spec.element),
|
|
3406
|
+
elementType,
|
|
3169
3407
|
attributes,
|
|
3170
3408
|
childNodes: [],
|
|
3171
3409
|
pairNodeUuid: null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { UnexpectedCallError } from '@markuplint/shared';
|
|
2
2
|
export class MLNamedNodeMap extends Array {
|
|
3
3
|
getNamedItem(qualifiedName) {
|
|
4
|
-
return this.find(attr => attr.
|
|
4
|
+
return this.find(attr => attr.isNamedBy(qualifiedName)) ?? null;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* **IT THROWS AN ERROR WHEN CALLING THIS.**
|
|
@@ -46,11 +46,13 @@ export class NodeStore {
|
|
|
46
46
|
setNode(astNode,
|
|
47
47
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
48
48
|
node) {
|
|
49
|
-
if (!astNode.uuid) {
|
|
49
|
+
if (!astNode.uuid && nodeStoreError.enabled) {
|
|
50
50
|
nodeStoreError('UUID is invalid: %s (%s: "%s")', astNode.uuid, astNode.nodeName, astNode.raw);
|
|
51
51
|
nodeStoreError('Invalid node: %O', node);
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
if (nodeStoreLog.enabled) {
|
|
54
|
+
nodeStoreLog('Mapped: %s (%s: "%s")', astNode.uuid, astNode.nodeName, astNode.raw.replaceAll('\n', '⏎').replaceAll('\t', '→'));
|
|
55
|
+
}
|
|
54
56
|
this.#store.set(astNode.uuid, node);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -20,38 +20,53 @@ export class RuleMapper {
|
|
|
20
20
|
this.#nodeList = Object.freeze([document, ...document.nodeList]);
|
|
21
21
|
}
|
|
22
22
|
apply() {
|
|
23
|
-
ruleMapperLog
|
|
23
|
+
if (ruleMapperLog.enabled) {
|
|
24
|
+
ruleMapperLog('ruleTree:');
|
|
25
|
+
}
|
|
26
|
+
const nodeEnabled = ruleMapperNodeLog.enabled;
|
|
27
|
+
const nodeRuleEnabled = ruleMapperNodeRuleLog.enabled;
|
|
24
28
|
for (const node of this.#nodeList) {
|
|
25
29
|
const rules = this.#ruleMap.get(node.uuid);
|
|
26
30
|
if (!rules) {
|
|
27
31
|
continue;
|
|
28
32
|
}
|
|
29
|
-
|
|
33
|
+
if (nodeEnabled) {
|
|
34
|
+
ruleMapperNodeLog('<%s>', node.nodeName);
|
|
35
|
+
}
|
|
30
36
|
for (const ruleName of Object.keys(rules)) {
|
|
31
37
|
const rule = rules[ruleName];
|
|
32
38
|
if (!rule) {
|
|
33
39
|
continue;
|
|
34
40
|
}
|
|
35
41
|
node.rules[ruleName] = rule.rule;
|
|
36
|
-
|
|
42
|
+
if (nodeRuleEnabled) {
|
|
43
|
+
ruleMapperNodeRuleLog('[from: %s(%s)] %s: %o', rule.from, rule.specificity, ruleName, rule.rule);
|
|
44
|
+
}
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
set(
|
|
41
49
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
42
50
|
node, ruleName, rule) {
|
|
51
|
+
const enabled = ruleMapperLog.enabled;
|
|
43
52
|
const rules = this.#ruleMap.get(node.uuid) ?? {};
|
|
44
53
|
const currentRule = rules[ruleName];
|
|
45
54
|
if (currentRule) {
|
|
46
55
|
const order = compareSpecificity(currentRule.specificity, rule.specificity);
|
|
47
56
|
if (order === 1) {
|
|
48
|
-
|
|
57
|
+
if (enabled) {
|
|
58
|
+
ruleMapperLog("Don't set %o ([%s] vs [%s])", rule, currentRule.specificity, rule.specificity);
|
|
59
|
+
}
|
|
49
60
|
return;
|
|
50
61
|
}
|
|
51
|
-
|
|
62
|
+
if (enabled) {
|
|
63
|
+
ruleMapperLog('Unset %o from %s', currentRule, node);
|
|
64
|
+
}
|
|
52
65
|
}
|
|
53
66
|
rules[ruleName] = rule;
|
|
54
67
|
this.#ruleMap.set(node.uuid, rules);
|
|
55
|
-
|
|
68
|
+
if (enabled) {
|
|
69
|
+
ruleMapperLog('Set to %s from %s (%o): %O', node.nodeName, rule.from, rule.specificity, rule.rule);
|
|
70
|
+
}
|
|
56
71
|
}
|
|
57
72
|
}
|
package/lib/test/index.js
CHANGED
|
@@ -13,13 +13,22 @@ import { MLDocument } from '../ml-dom/node/document.js';
|
|
|
13
13
|
* @returns A parsed MLDocument instance
|
|
14
14
|
*/
|
|
15
15
|
export function createTestDocument(sourceCode, options) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
? options.parser.parser.parse(sourceCode, options.config?.parserOptions)
|
|
19
|
-
: options.parser.parse(sourceCode, options.config?.parserOptions)
|
|
20
|
-
: parser.parse(sourceCode, options?.config?.parserOptions);
|
|
16
|
+
const usedParser = options?.parser ? ('parser' in options.parser ? options.parser.parser : options.parser) : parser;
|
|
17
|
+
const ast = usedParser.parse(sourceCode, options?.config?.parserOptions);
|
|
21
18
|
const ruleset = convertRuleset(options?.config);
|
|
22
|
-
const document = new MLDocument(ast, ruleset, [options?.specs ?? spec, {}], { ariaVersion: ARIA_RECOMMENDED_VERSION },
|
|
19
|
+
const document = new MLDocument(ast, ruleset, [options?.specs ?? spec, {}], { ariaVersion: ARIA_RECOMMENDED_VERSION }, {
|
|
20
|
+
// The settings of the language are what the parser says, as `MLCore` hands them to the document
|
|
21
|
+
endTag: usedParser.endTag,
|
|
22
|
+
booleanish: usedParser.booleanish,
|
|
23
|
+
tagNameCaseSensitive: usedParser.tagNameCaseSensitive,
|
|
24
|
+
componentNameVariants: usedParser.componentNameVariants,
|
|
25
|
+
isComponentName: usedParser.isComponentName?.bind(usedParser),
|
|
26
|
+
// Only a parser with a list of its own (Vue) has it, as against `isComponentName` that the base class has
|
|
27
|
+
isNativeTag: 'isNativeTag' in usedParser ? usedParser.isNativeTag?.bind(usedParser) : undefined,
|
|
28
|
+
attrNameCaseSensitive: usedParser.attrNameCaseSensitive,
|
|
29
|
+
attrNameRoute: usedParser.attrNameRoute,
|
|
30
|
+
...(options?.pretenders ? { pretenders: options.pretenders } : {}),
|
|
31
|
+
});
|
|
23
32
|
return document;
|
|
24
33
|
}
|
|
25
34
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/ml-core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "The core module of markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,8 +15,8 @@
|
|
|
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
|
},
|
|
22
22
|
"publishConfig": {
|
|
@@ -34,20 +34,20 @@
|
|
|
34
34
|
"./lib/configs.js": "./lib/configs.browser.js"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@markuplint/config-presets": "5.
|
|
38
|
-
"@markuplint/html-parser": "5.
|
|
39
|
-
"@markuplint/html-spec": "5.
|
|
40
|
-
"@markuplint/i18n": "5.
|
|
41
|
-
"@markuplint/ml-ast": "5.
|
|
42
|
-
"@markuplint/ml-config": "5.
|
|
43
|
-
"@markuplint/ml-spec": "5.
|
|
44
|
-
"@markuplint/parser-utils": "5.
|
|
45
|
-
"@markuplint/selector": "5.
|
|
46
|
-
"@markuplint/shared": "5.
|
|
37
|
+
"@markuplint/config-presets": "5.4.0",
|
|
38
|
+
"@markuplint/html-parser": "5.4.0",
|
|
39
|
+
"@markuplint/html-spec": "5.4.0",
|
|
40
|
+
"@markuplint/i18n": "5.3.0",
|
|
41
|
+
"@markuplint/ml-ast": "5.3.0",
|
|
42
|
+
"@markuplint/ml-config": "5.4.0",
|
|
43
|
+
"@markuplint/ml-spec": "5.4.0",
|
|
44
|
+
"@markuplint/parser-utils": "5.4.0",
|
|
45
|
+
"@markuplint/selector": "5.4.0",
|
|
46
|
+
"@markuplint/shared": "5.4.0",
|
|
47
47
|
"@types/debug": "^4.1.13",
|
|
48
48
|
"debug": "^4.4.3",
|
|
49
49
|
"is-plain-object": "^5.1.0",
|
|
50
50
|
"type-fest": "^5.10.0"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "647f6fb5d5743f060678599c97e9c84b58970747"
|
|
53
53
|
}
|