@markuplint/ml-core 5.1.1 → 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.
- package/CHANGELOG.md +35 -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.js +4 -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 +80 -1
- package/lib/ml-dom/node/attr.js +150 -9
- 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 +105 -4
- package/lib/ml-dom/node/element.js +327 -58
- 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 +19 -19
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/* global StylePropertyMap, StylePropertyMapReadOnly */
|
|
2
2
|
var _a;
|
|
3
3
|
import { getSpecByTagName, resolveNamespace } from '@markuplint/ml-spec';
|
|
4
|
-
import {
|
|
4
|
+
import { pretenderNamespaceToURI } from '@markuplint/ml-config';
|
|
5
|
+
import { getChildNamespace } from '@markuplint/parser-utils';
|
|
6
|
+
import { matchSelector, typeNameMatches } from '@markuplint/selector';
|
|
5
7
|
import { getAccname } from '../helper/accname.js';
|
|
6
8
|
import { after, before, nextElementSibling, previousElementSibling, remove, replaceWith, } from '../manipulations/child-node-methods.js';
|
|
7
9
|
import { MLAttr } from './attr.js';
|
|
@@ -10,7 +12,7 @@ import { MLElementCloseTag } from './element-close-tag.js';
|
|
|
10
12
|
import { toNamedNodeMap } from './named-node-map.js';
|
|
11
13
|
import { toHTMLCollection, toNodeList } from './node-list.js';
|
|
12
14
|
import { MLParentNode } from './parent-node.js';
|
|
13
|
-
import { UnexpectedCallError } from '@markuplint/shared';
|
|
15
|
+
import { UnexpectedCallError, asciiLowerCase, vuePropKey } from '@markuplint/shared';
|
|
14
16
|
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
15
17
|
/**
|
|
16
18
|
* Represents a DOM Element node wrapper in the markuplint DOM tree.
|
|
@@ -38,8 +40,149 @@ export class MLElement extends MLParentNode {
|
|
|
38
40
|
#getChildElementsAndTextNodeWithoutWhitespacesCache = null;
|
|
39
41
|
/**
|
|
40
42
|
* Whether this element belongs to a non-HTML namespace (e.g., SVG or MathML).
|
|
43
|
+
*
|
|
44
|
+
* Unlike {@link namespaceURI}, it does not follow the pretender: it says how the tag
|
|
45
|
+
* written here is parsed (whether the case of its name is kept, whether `/>` closes
|
|
46
|
+
* it), and the tag of a component is not parsed as the element it renders. An element
|
|
47
|
+
* given to a component as a child is, as it ends up inside what the component renders.
|
|
41
48
|
*/
|
|
42
|
-
isForeignElement
|
|
49
|
+
get isForeignElement() {
|
|
50
|
+
return this.#namespaceURI !== HTML_NAMESPACE;
|
|
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
|
+
}
|
|
43
186
|
/**
|
|
44
187
|
* Whether this element was implicitly created (e.g., an omitted `<body>` tag in HTML).
|
|
45
188
|
*/
|
|
@@ -47,8 +190,19 @@ export class MLElement extends MLParentNode {
|
|
|
47
190
|
#localName;
|
|
48
191
|
/**
|
|
49
192
|
* The namespace URI of this element (e.g., `http://www.w3.org/1999/xhtml` for HTML elements).
|
|
193
|
+
*
|
|
194
|
+
* Like {@link localName}, it follows the pretender, so that a namespaced selector
|
|
195
|
+
* (`svg|svg`) and a spec lookup see the element the component renders. An element
|
|
196
|
+
* given to a component as a child takes the namespace of where the component renders
|
|
197
|
+
* it, resolved when the document pretends.
|
|
50
198
|
*/
|
|
51
|
-
namespaceURI
|
|
199
|
+
get namespaceURI() {
|
|
200
|
+
if (this.pretenderContext?.type === 'pretender') {
|
|
201
|
+
return this.pretenderContext.as.namespaceURI;
|
|
202
|
+
}
|
|
203
|
+
return this.#namespaceURI;
|
|
204
|
+
}
|
|
205
|
+
#namespaceURI;
|
|
52
206
|
/**
|
|
53
207
|
* ## Why this cache exists
|
|
54
208
|
*
|
|
@@ -122,10 +276,9 @@ export class MLElement extends MLParentNode {
|
|
|
122
276
|
: null;
|
|
123
277
|
this.closeTag = pairAstNode ? new MLElementCloseTag(pairAstNode, document, this) : null;
|
|
124
278
|
const ns = resolveNamespace(astNode.nodeName, astNode.namespace);
|
|
125
|
-
this
|
|
279
|
+
this.#namespaceURI = astNode.namespace;
|
|
126
280
|
this.elementType = astNode.elementType;
|
|
127
281
|
this.#localName = ns.localName;
|
|
128
|
-
this.isForeignElement = this.namespaceURI !== HTML_NAMESPACE;
|
|
129
282
|
this.isOmitted = astNode.isGhost;
|
|
130
283
|
this.tagOpenChar = astNode.tagOpenChar;
|
|
131
284
|
this.tagCloseChar = astNode.tagCloseChar;
|
|
@@ -681,28 +834,21 @@ export class MLElement extends MLParentNode {
|
|
|
681
834
|
throw new UnexpectedCallError('Not supported "attributeStyleMap" property');
|
|
682
835
|
}
|
|
683
836
|
/**
|
|
837
|
+
* Every attribute written on the tag, the second of two spellings of one name too: unlike a DOM
|
|
838
|
+
* element, which keeps one of them, a rule checks each attribute that the author wrote, and
|
|
839
|
+
* `no-duplicate-attr` reports the second. Which of them the DOM keeps is what {@link getAttribute}
|
|
840
|
+
* returns.
|
|
841
|
+
*
|
|
684
842
|
* @implements DOM API: `Element`
|
|
685
843
|
* @see https://dom.spec.whatwg.org/#dom-element-attributes
|
|
686
844
|
*/
|
|
687
845
|
get attributes() {
|
|
688
846
|
const origin = this.pretenderContext?.type === 'pretender' ? this.pretenderContext.as.#attributes : this.#attributes;
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const names = new Set();
|
|
693
|
-
const attrs = [];
|
|
694
|
-
for (const attr of origin) {
|
|
695
|
-
if (names.has(attr.name)) {
|
|
696
|
-
/**
|
|
697
|
-
* Skips a duplicated attribute
|
|
698
|
-
*
|
|
699
|
-
*@see https://html.spec.whatwg.org/#parse-error-duplicate-attribute
|
|
700
|
-
*/
|
|
701
|
-
continue;
|
|
702
|
-
}
|
|
703
|
-
attrs.push(attr);
|
|
847
|
+
const cached = this.#normalizedAttrs.get(origin);
|
|
848
|
+
if (cached) {
|
|
849
|
+
return cached;
|
|
704
850
|
}
|
|
705
|
-
const map = toNamedNodeMap(
|
|
851
|
+
const map = toNamedNodeMap(origin);
|
|
706
852
|
this.#normalizedAttrs.set(origin, map);
|
|
707
853
|
return map;
|
|
708
854
|
}
|
|
@@ -2456,16 +2602,31 @@ export class MLElement extends MLParentNode {
|
|
|
2456
2602
|
throw new UnexpectedCallError('Not supported "getAnimations" method');
|
|
2457
2603
|
}
|
|
2458
2604
|
/**
|
|
2605
|
+
* Whether `attrName` is the name of an attribute of this element is what {@link MLAttr.isNamedBy}
|
|
2606
|
+
* says: `ROLE` is `role` on a `<div>` and, in JSX, is not on an `<svg>`. `hasAttribute`,
|
|
2607
|
+
* `getAttributeToken`, `getAttributePretended`, a pretender's `fromAttr` and `getNamedItem` ask the same.
|
|
2608
|
+
*
|
|
2459
2609
|
* @implements DOM API: `Element`
|
|
2460
2610
|
* @see https://dom.spec.whatwg.org/#ref-for-dom-element-getattribute%E2%91%A0
|
|
2461
2611
|
*/
|
|
2462
2612
|
getAttribute(attrName) {
|
|
2613
|
+
let first = null;
|
|
2614
|
+
let lastSetByCode = null;
|
|
2463
2615
|
for (const attr of this.attributes) {
|
|
2464
|
-
if (attr.
|
|
2465
|
-
|
|
2616
|
+
if (!attr.isNamedBy(attrName)) {
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
first ??= attr;
|
|
2620
|
+
// A directive, and an attribute that may be written twice (`class` and `:class`), are not
|
|
2621
|
+
// two spellings of one name: the first is the one that is asked for
|
|
2622
|
+
if (attr.isSetThroughDOMAPI && !attr.isDirective && !attr.isDuplicatable) {
|
|
2623
|
+
lastSetByCode = attr;
|
|
2466
2624
|
}
|
|
2467
2625
|
}
|
|
2468
|
-
|
|
2626
|
+
// Of two attributes of one name, the HTML parser keeps the first and drops the second, while code
|
|
2627
|
+
// that sets them one after another (`setAttribute`) lets the second overwrite the first.
|
|
2628
|
+
// @see https://html.spec.whatwg.org/#parse-error-duplicate-attribute
|
|
2629
|
+
return (lastSetByCode ?? first)?.value ?? null;
|
|
2469
2630
|
}
|
|
2470
2631
|
/**
|
|
2471
2632
|
* **IT THROWS AN ERROR WHEN CALLING THIS.**
|
|
@@ -2510,12 +2671,12 @@ export class MLElement extends MLParentNode {
|
|
|
2510
2671
|
* the original component element, not on the virtual pretender element.
|
|
2511
2672
|
*
|
|
2512
2673
|
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
2513
|
-
* @param attrName - The attribute name to look up (case
|
|
2674
|
+
* @param attrName - The attribute name to look up (see {@link MLAttr.isNamedBy} for how its case is compared)
|
|
2514
2675
|
* @returns The attribute value, or null if the attribute is not found
|
|
2515
2676
|
*/
|
|
2516
2677
|
getAttributePretended(attrName) {
|
|
2517
2678
|
for (const attr of this.#attributes) {
|
|
2518
|
-
if (attr.
|
|
2679
|
+
if (attr.isNamedBy(attrName)) {
|
|
2519
2680
|
return attr.value;
|
|
2520
2681
|
}
|
|
2521
2682
|
}
|
|
@@ -2526,14 +2687,13 @@ export class MLElement extends MLParentNode {
|
|
|
2526
2687
|
* Unlike `getAttribute`, this returns the full `MLAttr` token objects.
|
|
2527
2688
|
*
|
|
2528
2689
|
* @implements `@markuplint/ml-core` API: `MLElement`
|
|
2529
|
-
* @param attrName - The attribute name to look up (case
|
|
2690
|
+
* @param attrName - The attribute name to look up (see {@link MLAttr.isNamedBy} for how its case is compared)
|
|
2530
2691
|
* @returns An array of matching attribute tokens
|
|
2531
2692
|
*/
|
|
2532
2693
|
getAttributeToken(attrName) {
|
|
2533
2694
|
const attrs = [];
|
|
2534
|
-
attrName = attrName.toLowerCase();
|
|
2535
2695
|
for (const attr of this.getAttributeTokens()) {
|
|
2536
|
-
if (attr.
|
|
2696
|
+
if (attr.isNamedBy(attrName)) {
|
|
2537
2697
|
attrs.push(attr);
|
|
2538
2698
|
}
|
|
2539
2699
|
}
|
|
@@ -2861,6 +3021,102 @@ export class MLElement extends MLParentNode {
|
|
|
2861
3021
|
}
|
|
2862
3022
|
return matched;
|
|
2863
3023
|
}
|
|
3024
|
+
/**
|
|
3025
|
+
* The element this element is rendered in: when the parent is a component, the
|
|
3026
|
+
* element the component wraps its children with (the slot wrapper, or the element
|
|
3027
|
+
* it renders), otherwise the parent. `null` at the top level. `undefined` when the
|
|
3028
|
+
* parent is a component with more than one slot that give the children different
|
|
3029
|
+
* namespaces.
|
|
3030
|
+
*
|
|
3031
|
+
* A custom element is not a component here: it puts the children in its light DOM,
|
|
3032
|
+
* where they keep the namespace they are parsed in.
|
|
3033
|
+
*
|
|
3034
|
+
* The parent has pretended already: `MLDocument` calls `pretending()` in
|
|
3035
|
+
* document order.
|
|
3036
|
+
*/
|
|
3037
|
+
#getRenderedParent() {
|
|
3038
|
+
const parent = this.parentElement;
|
|
3039
|
+
if (parent?.pretenderContext?.type !== 'pretender' || parent.elementType === 'web-component') {
|
|
3040
|
+
return parent;
|
|
3041
|
+
}
|
|
3042
|
+
const { as, slotContent } = parent.pretenderContext;
|
|
3043
|
+
if (!slotContent) {
|
|
3044
|
+
return as;
|
|
3045
|
+
}
|
|
3046
|
+
return slotContent.wrapper ?? parent.#sharedWrapper ?? undefined;
|
|
3047
|
+
}
|
|
3048
|
+
/**
|
|
3049
|
+
* For a component with more than one slot, whose wrapper of each child is unknown, a
|
|
3050
|
+
* wrapper that gives the children the same namespace as all the others do, so that
|
|
3051
|
+
* the children can still be resolved. `null` when they differ (`<svg>` and the `<div>`
|
|
3052
|
+
* inside its `foreignObject`).
|
|
3053
|
+
*/
|
|
3054
|
+
#sharedWrapper = null;
|
|
3055
|
+
#findSharedWrapper(slots, as) {
|
|
3056
|
+
const wrappers = slots.map((slot, i) => this.#createVirtualElement(slot, `${this.uuid}_pretender_slot_${i}`, this.uuid, as));
|
|
3057
|
+
const [first] = wrappers;
|
|
3058
|
+
if (!first) {
|
|
3059
|
+
return null;
|
|
3060
|
+
}
|
|
3061
|
+
const namespace = first.#getChildNamespace(null);
|
|
3062
|
+
return wrappers.every(wrapper => wrapper.#getChildNamespace(null) === namespace) ? first : null;
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* The namespace of an element named `name` rendered as a child of this element, from
|
|
3066
|
+
* its own resolved namespace rather than the one of what it pretends to be.
|
|
3067
|
+
* `null` takes the namespace this element gives any element.
|
|
3068
|
+
*/
|
|
3069
|
+
#getChildNamespace(name) {
|
|
3070
|
+
return getChildNamespace(name, this.#namespaceURI, this._astToken.nodeName);
|
|
3071
|
+
}
|
|
3072
|
+
/**
|
|
3073
|
+
* Re-resolves the namespace for the element this element is rendered in.
|
|
3074
|
+
*
|
|
3075
|
+
* The parser gives the children written inside a component the namespace of where
|
|
3076
|
+
* the component is written, as it does not know what the component renders: in
|
|
3077
|
+
* `<Icon><path /></Icon>`, where `Icon` renders `<svg>`, `path` is an HTML element
|
|
3078
|
+
* to it. Its namespace is kept while the parent is where the parser put it, so that
|
|
3079
|
+
* where no component is involved, the namespace stays what the parser gave, and when
|
|
3080
|
+
* where the component renders it is unknown.
|
|
3081
|
+
*
|
|
3082
|
+
* `getChildNamespace` leaves foreign content only at `foreignObject`, so an element
|
|
3083
|
+
* re-resolved here does not follow the other integration points (`<mi>`, `<desc>`),
|
|
3084
|
+
* nor the HTML elements that break out of foreign content in an HTML document (`<div>`
|
|
3085
|
+
* inside `<svg>`).
|
|
3086
|
+
*
|
|
3087
|
+
* A component is given no name, so that its own name is not taken for an element:
|
|
3088
|
+
* `getChildNamespace` compares names in lowercase, so `<Math>` would enter MathML.
|
|
3089
|
+
* The parsers of this repository do the same, but the AST may come from a parser
|
|
3090
|
+
* that does not.
|
|
3091
|
+
*/
|
|
3092
|
+
#resolveNamespace(renderedParent) {
|
|
3093
|
+
const parent = this.parentElement;
|
|
3094
|
+
if (!parent || !renderedParent) {
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
if (renderedParent === parent && parent.#namespaceURI === parent._astToken.namespace) {
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
this.#namespaceURI = renderedParent.#getChildNamespace(this.elementType === 'authored' ? null : this._astToken.nodeName);
|
|
3101
|
+
}
|
|
3102
|
+
/**
|
|
3103
|
+
* The namespace of an element named `name` rendered where this element is, for the
|
|
3104
|
+
* element this element pretends to be. `null` for what is not an element, a
|
|
3105
|
+
* component.
|
|
3106
|
+
*
|
|
3107
|
+
* Where this element is has the namespace of this element, so the element gets the
|
|
3108
|
+
* namespace an element written there gets. For a component, it is taken from the
|
|
3109
|
+
* parent instead, as a parser that does not keep the name of a component out of
|
|
3110
|
+
* the namespace may have taken it for an element (`<Svg>`, `<Math>`: the names are
|
|
3111
|
+
* compared in lowercase).
|
|
3112
|
+
*/
|
|
3113
|
+
#getNamespaceInPlace(name, renderedParent) {
|
|
3114
|
+
let here = this.#namespaceURI;
|
|
3115
|
+
if (this.elementType === 'authored' && renderedParent !== undefined) {
|
|
3116
|
+
here = renderedParent ? renderedParent.#getChildNamespace(null) : HTML_NAMESPACE;
|
|
3117
|
+
}
|
|
3118
|
+
return getChildNamespace(name, here, null);
|
|
3119
|
+
}
|
|
2864
3120
|
/**
|
|
2865
3121
|
* Initializes the pretender context for this element based on the given pretender
|
|
2866
3122
|
* configurations. If a matching pretender is found or the element has an `as` attribute,
|
|
@@ -2869,18 +3125,23 @@ export class MLElement extends MLParentNode {
|
|
|
2869
3125
|
* @param pretenders - Optional array of pretender configurations to match against
|
|
2870
3126
|
*/
|
|
2871
3127
|
pretending(pretenders) {
|
|
3128
|
+
const renderedParent = this.#getRenderedParent();
|
|
3129
|
+
this.#resolveNamespace(renderedParent);
|
|
2872
3130
|
// Pretender must not apply to a recognised standard HTML element (e.g. <marquee>,
|
|
2873
3131
|
// <h1>, <button>). Allowing such elements to masquerade as another would silently
|
|
2874
3132
|
// mask spec-driven rules — deprecation, ARIA role restrictions, browser support —
|
|
2875
3133
|
// keyed on the original tag. See issue #3740.
|
|
2876
3134
|
//
|
|
3135
|
+
// What is recognised is what the document says (`MLDocument#isNativeTag`): the specification, unless
|
|
3136
|
+
// the parser has a rule of its own. Vue's rule does not hold `<marquee>`, which Vue resolves to a
|
|
3137
|
+
// component when one is imported, so there it is pretender-eligible. See issue #4351.
|
|
3138
|
+
//
|
|
2877
3139
|
// Names that the HTML parser cannot distinguish from typos (PascalCase JSX-like
|
|
2878
3140
|
// usage in plain HTML such as `<SimpleButton>`) get `elementType === 'html'`
|
|
2879
3141
|
// from the parser but have no spec entry; those remain pretender-eligible
|
|
2880
3142
|
// (this is what `pretenders.scan` relies on). The legacy "no inline `as=` on
|
|
2881
3143
|
// HTML elements" guard is preserved further down for that case.
|
|
2882
|
-
if (this.elementType === 'html' &&
|
|
2883
|
-
getSpecByTagName(this.ownerMLDocument.specs.specs, this.localName, this.namespaceURI) != null) {
|
|
3144
|
+
if (this.elementType === 'html' && this.ownerMLDocument.isNativeTag(this.localName, this.namespaceURI)) {
|
|
2884
3145
|
return;
|
|
2885
3146
|
}
|
|
2886
3147
|
const pretenderConfig = pretenders?.find(option => this.matches(option.selector));
|
|
@@ -2896,7 +3157,6 @@ export class MLElement extends MLParentNode {
|
|
|
2896
3157
|
return;
|
|
2897
3158
|
}
|
|
2898
3159
|
let nodeName;
|
|
2899
|
-
let namespace = 'http://www.w3.org/1999/xhtml';
|
|
2900
3160
|
const attributes = [];
|
|
2901
3161
|
let aria;
|
|
2902
3162
|
if (typeof pretenderElement === 'string') {
|
|
@@ -2904,7 +3164,6 @@ export class MLElement extends MLParentNode {
|
|
|
2904
3164
|
}
|
|
2905
3165
|
else {
|
|
2906
3166
|
nodeName = pretenderElement.element;
|
|
2907
|
-
namespace = pretenderElement.namespace === 'svg' ? 'http://www.w3.org/2000/svg' : namespace;
|
|
2908
3167
|
if (pretenderElement.inheritAttrs) {
|
|
2909
3168
|
attributes.push(...this._astToken.attributes);
|
|
2910
3169
|
}
|
|
@@ -2914,13 +3173,19 @@ export class MLElement extends MLParentNode {
|
|
|
2914
3173
|
aria = pretenderElement.aria;
|
|
2915
3174
|
}
|
|
2916
3175
|
const slots = typeof pretenderElement === 'string' ? undefined : pretenderElement.slots;
|
|
3176
|
+
// A component is given no name for the namespace, as `#resolveNamespace` does:
|
|
3177
|
+
// `getChildNamespace` compares names in lowercase, so `Svg` would enter SVG.
|
|
3178
|
+
const isComponent = this.ownerMLDocument.isComponentName(nodeName);
|
|
3179
|
+
const namespace = typeof pretenderElement !== 'string' && pretenderElement.namespace
|
|
3180
|
+
? pretenderNamespaceToURI(pretenderElement.namespace)
|
|
3181
|
+
: this.#getNamespaceInPlace(isComponent ? null : nodeName, renderedParent);
|
|
2917
3182
|
const as = new _a({
|
|
2918
3183
|
...this._astToken,
|
|
2919
3184
|
uuid: this.uuid + '_pretender',
|
|
2920
3185
|
raw: `<${nodeName}>`,
|
|
2921
3186
|
nodeName,
|
|
2922
3187
|
namespace,
|
|
2923
|
-
elementType: 'html',
|
|
3188
|
+
elementType: isComponent ? 'authored' : 'html',
|
|
2924
3189
|
attributes,
|
|
2925
3190
|
}, this.ownerMLDocument);
|
|
2926
3191
|
// When slots is null, the component does not accept children (void-like).
|
|
@@ -2935,7 +3200,7 @@ export class MLElement extends MLParentNode {
|
|
|
2935
3200
|
type: 'origin',
|
|
2936
3201
|
origin: this,
|
|
2937
3202
|
};
|
|
2938
|
-
const slotContent = typeof pretenderElement === 'string' ? undefined : this.#createSlotContent(pretenderElement, as
|
|
3203
|
+
const slotContent = typeof pretenderElement === 'string' ? undefined : this.#createSlotContent(pretenderElement, as);
|
|
2939
3204
|
this.pretenderContext = {
|
|
2940
3205
|
type: 'pretender',
|
|
2941
3206
|
as,
|
|
@@ -2964,8 +3229,13 @@ export class MLElement extends MLParentNode {
|
|
|
2964
3229
|
_value = value;
|
|
2965
3230
|
}
|
|
2966
3231
|
else if (value != null && 'fromAttr' in value) {
|
|
2967
|
-
|
|
2968
|
-
|
|
3232
|
+
// The attribute of the usage site that the prop is. Vue gives a prop the attribute by the camelCase
|
|
3233
|
+
// of its name (`my-label` and `myLabel` are the prop `myLabel`, and `mylabel` is another),
|
|
3234
|
+
// and the prop declared as `'my-label'` is the same one, so both sides are read so. Elsewhere
|
|
3235
|
+
// the attribute is read as the DOM or the framework reads it (`isNamedBy`).
|
|
3236
|
+
const { fromAttr } = value;
|
|
3237
|
+
const isVue = this.ownerMLDocument.componentNameVariants;
|
|
3238
|
+
const origin = this.#attributes.find(attr => isVue ? vuePropKey(attr.name) === vuePropKey(fromAttr) : attr.isNamedBy(fromAttr));
|
|
2969
3239
|
if (!origin && value.omitIfMissing) {
|
|
2970
3240
|
return [];
|
|
2971
3241
|
}
|
|
@@ -3032,7 +3302,7 @@ export class MLElement extends MLParentNode {
|
|
|
3032
3302
|
* constructor would otherwise re-register the origin's real close tag under
|
|
3033
3303
|
* the virtual element.
|
|
3034
3304
|
*/
|
|
3035
|
-
#createVirtualElement(spec, uuid, parentNodeUuid,
|
|
3305
|
+
#createVirtualElement(spec, uuid, parentNodeUuid, renderedIn) {
|
|
3036
3306
|
const attributes = [];
|
|
3037
3307
|
if (spec.inheritAttrs) {
|
|
3038
3308
|
attributes.push(...this._astToken.attributes);
|
|
@@ -3040,13 +3310,23 @@ export class MLElement extends MLParentNode {
|
|
|
3040
3310
|
if (spec.attrs) {
|
|
3041
3311
|
attributes.push(...this.#createPretenderAttrs(spec.attrs, uuid));
|
|
3042
3312
|
}
|
|
3313
|
+
const isComponent = this.ownerMLDocument.isComponentName(spec.element);
|
|
3314
|
+
let elementType = 'html';
|
|
3315
|
+
if (isComponent) {
|
|
3316
|
+
elementType = 'authored';
|
|
3317
|
+
}
|
|
3318
|
+
else if (spec.element.includes('-')) {
|
|
3319
|
+
elementType = 'web-component';
|
|
3320
|
+
}
|
|
3043
3321
|
const virtual = new _a({
|
|
3044
3322
|
...this._astToken,
|
|
3045
3323
|
uuid,
|
|
3046
3324
|
raw: `<${spec.element}>`,
|
|
3047
3325
|
nodeName: spec.element,
|
|
3048
|
-
namespace:
|
|
3049
|
-
|
|
3326
|
+
namespace: spec.namespace
|
|
3327
|
+
? pretenderNamespaceToURI(spec.namespace)
|
|
3328
|
+
: renderedIn.#getChildNamespace(isComponent ? null : spec.element),
|
|
3329
|
+
elementType,
|
|
3050
3330
|
attributes,
|
|
3051
3331
|
childNodes: [],
|
|
3052
3332
|
pairNodeUuid: null,
|
|
@@ -3064,7 +3344,7 @@ export class MLElement extends MLParentNode {
|
|
|
3064
3344
|
* the children, so that the children given at the usage site stay the whole
|
|
3065
3345
|
* content of the outermost element (the bare-string pretender behavior).
|
|
3066
3346
|
*/
|
|
3067
|
-
#createSlotContent(node, as
|
|
3347
|
+
#createSlotContent(node, as) {
|
|
3068
3348
|
const { slots } = node;
|
|
3069
3349
|
let wrapper = as;
|
|
3070
3350
|
let contents = node.contents;
|
|
@@ -3073,20 +3353,23 @@ export class MLElement extends MLParentNode {
|
|
|
3073
3353
|
if (slots.length > 1 || !slot) {
|
|
3074
3354
|
// The wrapper of each child is unknown, so the children are not validated,
|
|
3075
3355
|
// but a slot may still render unknown content (which may be a name).
|
|
3356
|
+
this.#sharedWrapper = this.#findSharedWrapper(slots, as);
|
|
3076
3357
|
return {
|
|
3077
3358
|
wrapper: null,
|
|
3078
3359
|
mutable: slots.some(s => s.contents?.some(entry => 'dynamic' in entry) ?? false),
|
|
3079
3360
|
fill: (given) => given,
|
|
3080
3361
|
};
|
|
3081
3362
|
}
|
|
3082
|
-
wrapper
|
|
3363
|
+
// The elements between the root and the wrapper are not known, so the wrapper is
|
|
3364
|
+
// taken to be a child of the root unless the definition states its namespace.
|
|
3365
|
+
wrapper = this.#createVirtualElement(slot, `${this.uuid}_pretender_slot`, this.uuid, as);
|
|
3083
3366
|
contents = slot.contents ?? [{ slot: true }];
|
|
3084
3367
|
}
|
|
3085
3368
|
if (!contents) {
|
|
3086
3369
|
return undefined;
|
|
3087
3370
|
}
|
|
3088
3371
|
const entries = contents.map((entry, i) => 'element' in entry
|
|
3089
|
-
? this.#createVirtualElement(entry, `${this.uuid}_pretender_content_${i}`, wrapper === as ? this.uuid : wrapper.uuid,
|
|
3372
|
+
? this.#createVirtualElement(entry, `${this.uuid}_pretender_content_${i}`, wrapper === as ? this.uuid : wrapper.uuid, wrapper)
|
|
3090
3373
|
: entry);
|
|
3091
3374
|
return {
|
|
3092
3375
|
wrapper,
|
|
@@ -3372,17 +3655,3 @@ export class MLElement extends MLParentNode {
|
|
|
3372
3655
|
}
|
|
3373
3656
|
}
|
|
3374
3657
|
_a = MLElement;
|
|
3375
|
-
/**
|
|
3376
|
-
* The namespace of a virtual element made from a pretender definition.
|
|
3377
|
-
* `svg` and `math` are foreign elements wherever they are written, and a
|
|
3378
|
-
* definition can only state `namespace: 'svg'`, so the name decides.
|
|
3379
|
-
*/
|
|
3380
|
-
function getVirtualNamespace(spec, fallback) {
|
|
3381
|
-
if (spec.namespace === 'svg' || spec.element === 'svg') {
|
|
3382
|
-
return 'http://www.w3.org/2000/svg';
|
|
3383
|
-
}
|
|
3384
|
-
if (spec.element === 'math') {
|
|
3385
|
-
return 'http://www.w3.org/1998/Math/MathML';
|
|
3386
|
-
}
|
|
3387
|
-
return fallback;
|
|
3388
|
-
}
|
|
@@ -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
|
}
|