@markuplint/ml-core 5.5.0 → 5.6.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 +15 -0
- package/lib/ml-core.js +1 -0
- package/lib/ml-dom/helper/accname.d.ts +6 -3
- package/lib/ml-dom/helper/accname.js +14 -7
- package/lib/ml-dom/helper/slot-assignment.d.ts +48 -0
- package/lib/ml-dom/helper/slot-assignment.js +292 -0
- package/lib/ml-dom/helper/tree-scope.d.ts +42 -5
- package/lib/ml-dom/helper/tree-scope.js +35 -8
- package/lib/ml-dom/node/document.d.ts +11 -1
- package/lib/ml-dom/node/document.js +26 -9
- package/lib/ml-dom/node/element.d.ts +60 -6
- package/lib/ml-dom/node/element.js +90 -15
- package/lib/ml-dom/node/node.d.ts +12 -5
- package/lib/ml-dom/node/node.js +12 -5
- package/lib/ml-dom/node/text.d.ts +12 -3
- package/lib/ml-dom/node/text.js +14 -3
- package/lib/test/index.js +1 -0
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/search-node-at-location.d.ts +39 -0
- package/lib/utils/search-node-at-location.js +53 -0
- package/package.json +12 -15
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [5.6.0](https://github.com/markuplint/markuplint/compare/v5.5.0...v5.6.0) (2026-11-12)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **ml-core:** compute the slot assignment of a declarative shadow root ([381ce83](https://github.com/markuplint/markuplint/commit/381ce8308c9f3e5602654bbbdec6a5a3f8597941))
|
|
11
|
+
* **ml-core:** do not take a spread attribute for a directive of a template element ([9af419e](https://github.com/markuplint/markuplint/commit/9af419ec690df40448f4afa3a5c37933f35e4830))
|
|
12
|
+
* **ml-core:** find the innermost node whose range holds a location ([d45b662](https://github.com/markuplint/markuplint/commit/d45b6623a7fe85b675263a74ad85e3d86fa7a422))
|
|
13
|
+
* **ml-core:** read the declarative shadow root of a pretended host first ([905cbf6](https://github.com/markuplint/markuplint/commit/905cbf6f541c2dd8475ec4c4a3d2383b2a95341c)), closes [#4527](https://github.com/markuplint/markuplint/issues/4527)
|
|
14
|
+
* **ml-core:** report an invalid selector of a pretender as an error of the configuration ([3af8221](https://github.com/markuplint/markuplint/commit/3af8221583b0d16805c250cfa0a84baa8733fdc4))
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **ml-core:** add MLElement#isUnresolvedComponent ([9da55f2](https://github.com/markuplint/markuplint/commit/9da55f206aeeed4285cf094f9f033b3e92650365))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# [5.5.0](https://github.com/markuplint/markuplint/compare/v5.4.0...v5.5.0) (2026-11-10)
|
|
7
22
|
|
|
8
23
|
### Bug Fixes
|
package/lib/ml-core.js
CHANGED
|
@@ -346,6 +346,7 @@ export class MLCore {
|
|
|
346
346
|
booleanish: this.#parser.booleanish,
|
|
347
347
|
attrExpressionLanguage: this.#parser.attrExpressionLanguage,
|
|
348
348
|
literalValues: this.#parser.literalValues,
|
|
349
|
+
slotElement: this.#parser.slotElement,
|
|
349
350
|
tagNameCaseSensitive: this.#parser.tagNameCaseSensitive,
|
|
350
351
|
componentNameVariants: this.#parser.componentNameVariants,
|
|
351
352
|
// The parser's own rule, called on the parser: a method of a class that reads its own state
|
|
@@ -34,10 +34,13 @@ export declare function getHostLanguageLabelSource(el: MLElement<any, any>, vers
|
|
|
34
34
|
* and is not rendered where the element is written. A host renders the child
|
|
35
35
|
* nodes of its declarative shadow root, of either mode. A `template` whose
|
|
36
36
|
* shadow root is only maybe attached contributes none too, because that root
|
|
37
|
-
* is not there to read.
|
|
38
|
-
*
|
|
37
|
+
* is not there to read. A slot whose assignment is not known contributes its own children, so a rule that
|
|
38
|
+
* must not report on what it cannot know asks `MLElement#getAssignedNodes` itself.
|
|
39
39
|
*
|
|
40
|
-
*
|
|
40
|
+
* The shadow root comes first, for a pretended component too: it is attached to a
|
|
41
|
+
* pretended host only when the component renders the children written in it in
|
|
42
|
+
* itself, and then it is rendered in place of what the component renders.
|
|
43
|
+
* Otherwise a pretended component is unchanged: `as.childNodes` is empty when it never
|
|
41
44
|
* renders its children (`slots: null`), so text written inside `<Img>Save</Img>`
|
|
42
45
|
* does not become its name. `contents` may wrap the children given at the usage site.
|
|
43
46
|
*
|
|
@@ -115,10 +115,13 @@ el, error) {
|
|
|
115
115
|
* and is not rendered where the element is written. A host renders the child
|
|
116
116
|
* nodes of its declarative shadow root, of either mode. A `template` whose
|
|
117
117
|
* shadow root is only maybe attached contributes none too, because that root
|
|
118
|
-
* is not there to read.
|
|
119
|
-
*
|
|
118
|
+
* is not there to read. A slot whose assignment is not known contributes its own children, so a rule that
|
|
119
|
+
* must not report on what it cannot know asks `MLElement#getAssignedNodes` itself.
|
|
120
120
|
*
|
|
121
|
-
*
|
|
121
|
+
* The shadow root comes first, for a pretended component too: it is attached to a
|
|
122
|
+
* pretended host only when the component renders the children written in it in
|
|
123
|
+
* itself, and then it is rendered in place of what the component renders.
|
|
124
|
+
* Otherwise a pretended component is unchanged: `as.childNodes` is empty when it never
|
|
122
125
|
* renders its children (`slots: null`), so text written inside `<Img>Save</Img>`
|
|
123
126
|
* does not become its name. `contents` may wrap the children given at the usage site.
|
|
124
127
|
*
|
|
@@ -140,15 +143,19 @@ el, error) {
|
|
|
140
143
|
export function getRenderedChildNodes(
|
|
141
144
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
142
145
|
el) {
|
|
146
|
+
const shadowRoot = el.declarativeShadowRoot;
|
|
147
|
+
if (shadowRoot) {
|
|
148
|
+
return shadowRoot.childNodes;
|
|
149
|
+
}
|
|
150
|
+
const assigned = el.getAssignedNodes();
|
|
151
|
+
if (assigned && assigned.length > 0) {
|
|
152
|
+
return assigned;
|
|
153
|
+
}
|
|
143
154
|
const context = el.pretenderContext;
|
|
144
155
|
if (context?.type === 'pretender') {
|
|
145
156
|
const given = [...context.as.childNodes];
|
|
146
157
|
return context.slotContent ? context.slotContent.fill(given) : given;
|
|
147
158
|
}
|
|
148
|
-
const shadowRoot = el.declarativeShadowRoot;
|
|
149
|
-
if (shadowRoot) {
|
|
150
|
-
return shadowRoot.childNodes;
|
|
151
|
-
}
|
|
152
159
|
if (el.isHTMLTemplateElement) {
|
|
153
160
|
return [];
|
|
154
161
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { MLChildNode } from '../node/child-node.js';
|
|
2
|
+
import type { MLElement } from '../node/element.js';
|
|
3
|
+
import type { MLNode } from '../node/node.js';
|
|
4
|
+
import type { PlainData, RuleConfigValue } from '@markuplint/ml-config';
|
|
5
|
+
/**
|
|
6
|
+
* The nodes a `slot` places.
|
|
7
|
+
*
|
|
8
|
+
* @template T - The rule configuration value type
|
|
9
|
+
* @template O - The rule options type
|
|
10
|
+
* @param slot - An element
|
|
11
|
+
* @returns The assigned nodes in tree order (`[]` when it takes none), or `null` when the element is not an
|
|
12
|
+
* HTML `slot` of a declarative shadow root whose assignment is known (see the module comment)
|
|
13
|
+
* @see https://dom.spec.whatwg.org/#dom-slot-assignednodes
|
|
14
|
+
*/
|
|
15
|
+
export declare function getAssignedNodes<T extends RuleConfigValue, O extends PlainData>(slot: MLElement<T, O>): readonly MLChildNode<T, O>[] | null;
|
|
16
|
+
/**
|
|
17
|
+
* The slot that places a node, as the flat tree has it.
|
|
18
|
+
*
|
|
19
|
+
* @template T - The rule configuration value type
|
|
20
|
+
* @template O - The rule options type
|
|
21
|
+
* @param node - An element or a text node
|
|
22
|
+
* @returns The slot; `null` when the node is a child of a shadow host that no slot of its shadow root
|
|
23
|
+
* takes, so that it is not rendered; `undefined` when the node is not a child of a shadow host, or the
|
|
24
|
+
* assignment is not known
|
|
25
|
+
* @see https://dom.spec.whatwg.org/#find-a-slot
|
|
26
|
+
*/
|
|
27
|
+
export declare function getFlatTreeSlot<T extends RuleConfigValue, O extends PlainData>(node: MLNode<T, O>): MLElement<T, O> | null | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The slot of an open shadow root that a node is assigned to.
|
|
30
|
+
*
|
|
31
|
+
* @template T - The rule configuration value type
|
|
32
|
+
* @template O - The rule options type
|
|
33
|
+
* @param node - An element or a text node
|
|
34
|
+
* @returns The slot, or `null` when none takes the node, its shadow root is closed, or the assignment is not known
|
|
35
|
+
* @see https://dom.spec.whatwg.org/#dom-slotable-assignedslot
|
|
36
|
+
*/
|
|
37
|
+
export declare function getAssignedSlot<T extends RuleConfigValue, O extends PlainData>(node: MLNode<T, O>): MLElement<T, O> | null;
|
|
38
|
+
/**
|
|
39
|
+
* The nodes a `slot` renders, with each assigned slot replaced by what that slot renders in turn, and the
|
|
40
|
+
* children of a slot that takes none in its place.
|
|
41
|
+
*
|
|
42
|
+
* @template T - The rule configuration value type
|
|
43
|
+
* @template O - The rule options type
|
|
44
|
+
* @param slot - An element
|
|
45
|
+
* @returns The nodes, or `[]` when the assignment is not known
|
|
46
|
+
* @see https://dom.spec.whatwg.org/#find-flattened-slotables
|
|
47
|
+
*/
|
|
48
|
+
export declare function getFlattenedSlottables<T extends RuleConfigValue, O extends PlainData>(slot: MLElement<T, O>): readonly MLChildNode<T, O>[];
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { isShadowRoot } from '@markuplint/ml-spec';
|
|
2
|
+
import { getDOMValue, getShadowHost, getShadowRootTemplate, HTML_NAMESPACE, isTransparent } from './tree-scope.js';
|
|
3
|
+
const cache = new WeakMap();
|
|
4
|
+
/**
|
|
5
|
+
* Whether the element is the HTML `slot` element: not the placeholder of a framework's component
|
|
6
|
+
* (`MLParser#slotElement`, and what the parser of a framework turns into a fragment).
|
|
7
|
+
*
|
|
8
|
+
* @param el - An element
|
|
9
|
+
* @returns `true` for the HTML `slot` element
|
|
10
|
+
*/
|
|
11
|
+
function isHTMLSlot(
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
13
|
+
el) {
|
|
14
|
+
return (el.localName === 'slot' &&
|
|
15
|
+
el.namespaceURI === HTML_NAMESPACE &&
|
|
16
|
+
!el.isFragment &&
|
|
17
|
+
el.ownerMLDocument.slotElement === 'html');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The string that the DOM has for an attribute that names a slot: `''` when it is missing, `null`
|
|
21
|
+
* when it is written as code.
|
|
22
|
+
*
|
|
23
|
+
* @param el - The element
|
|
24
|
+
* @param name - The attribute name (`name` of a `slot`, `slot` of a slottable)
|
|
25
|
+
* @returns The name, or `null` when it is not known
|
|
26
|
+
*/
|
|
27
|
+
function getSlotName(
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
29
|
+
el, name) {
|
|
30
|
+
// An attribute that has no name node is a spread (`{...props}`), which may set it
|
|
31
|
+
if (el.attributes.some(attr => !attr.nameNode)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const attr = el.getAttributeNode(name);
|
|
35
|
+
if (!attr || attr.isAbsent) {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
return getDOMValue(attr);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Collects the HTML `slot` elements of the shadow root in tree order, without entering the content of a
|
|
42
|
+
* `template` element, which is a tree of its own.
|
|
43
|
+
*
|
|
44
|
+
* @param parent - The shadow root, or an element in it
|
|
45
|
+
* @param slots - The slots found so far, which the walk appends to
|
|
46
|
+
* @param inBlock - Whether the walk went through a node that the engine renders away (a block or a
|
|
47
|
+
* fragment), under which a slot may not be rendered
|
|
48
|
+
* @returns `false` when which slot is the first of its name is not known: a slot in such a node, or
|
|
49
|
+
* one whose `name` is not known
|
|
50
|
+
*/
|
|
51
|
+
function collectSlots(
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
53
|
+
parent,
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
55
|
+
slots, inBlock) {
|
|
56
|
+
for (const child of parent.getPureChildNodes()) {
|
|
57
|
+
if (!child.is(child.ELEMENT_NODE) && !child.is(child.MARKUPLINT_PREPROCESSOR_BLOCK)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (isTransparent(child)) {
|
|
61
|
+
if (!collectSlots(child, slots, true)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!child.is(child.ELEMENT_NODE)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (isHTMLSlot(child)) {
|
|
70
|
+
// Whether it is rendered, and its `name`, are not known
|
|
71
|
+
if (inBlock || getSlotName(child, 'name') === null) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
slots.push(child);
|
|
75
|
+
}
|
|
76
|
+
// The content of a `template` element is a tree of its own, which has its own slots
|
|
77
|
+
if (!child.isHTMLTemplateElement && !collectSlots(child, slots, inBlock)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The nodes that are the children of the host in the output, seen through what the engine renders away,
|
|
85
|
+
* the nodes in every branch of a conditional block included.
|
|
86
|
+
*
|
|
87
|
+
* @param parent - The host, or a node that the engine renders away in it
|
|
88
|
+
* @param nodes - The nodes found so far
|
|
89
|
+
* @param declaring - The `template` element that declares the shadow root, which is not a child of the host in the tree
|
|
90
|
+
* @returns `false` when the nodes are not known
|
|
91
|
+
*/
|
|
92
|
+
function collectSlottables(
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
94
|
+
parent,
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
96
|
+
nodes,
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
98
|
+
declaring) {
|
|
99
|
+
for (const child of parent.getPureChildNodes()) {
|
|
100
|
+
if (child === declaring) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (child.is(child.MARKUPLINT_PREPROCESSOR_BLOCK)) {
|
|
104
|
+
// A block that has no behavior may be a marker of a language whose blocks the syntax tree does not nest
|
|
105
|
+
if (!child.blockBehavior || !collectSlottables(child, nodes, declaring)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (child.is(child.ELEMENT_NODE)) {
|
|
111
|
+
if (isTransparent(child)) {
|
|
112
|
+
if (!collectSlottables(child, nodes, declaring)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// What it renders may carry any `slot` attribute
|
|
118
|
+
if (child.isUnresolvedComponent()) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
nodes.push(child);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (child.is(child.TEXT_NODE)) {
|
|
125
|
+
nodes.push(child);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Computes the assignment of the shadow root of a host.
|
|
132
|
+
*
|
|
133
|
+
* @param host - A shadow host
|
|
134
|
+
* @returns The assignment, or `null` when the host has no shadow root or any of the causes in the module
|
|
135
|
+
* comment leaves the assignment unknown
|
|
136
|
+
*/
|
|
137
|
+
function compute(
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
139
|
+
host) {
|
|
140
|
+
const root = host.declarativeShadowRoot;
|
|
141
|
+
const template = getShadowRootTemplate(host);
|
|
142
|
+
if (!root || !template) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (host.ownerMLDocument.slotElement !== 'html' || root.slotAssignment === 'manual') {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
// A shadow root that a conditional wrapper may not render has no known slots
|
|
149
|
+
if (template.syntacticalParentNode !== host) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const slotList = [];
|
|
153
|
+
if (!collectSlots(root, slotList, false)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const slottables = [];
|
|
157
|
+
if (!collectSlottables(host, slottables, template)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const names = new Map();
|
|
161
|
+
for (const slot of slotList) {
|
|
162
|
+
const name = getSlotName(slot, 'name');
|
|
163
|
+
if (name === null) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
names.set(slot, name);
|
|
167
|
+
}
|
|
168
|
+
const slots = new Map(slotList.map(slot => [slot, []]));
|
|
169
|
+
// The first slot of the shadow root, in tree order, that has each name
|
|
170
|
+
const firstSlotOfName = new Map();
|
|
171
|
+
for (const slot of slotList) {
|
|
172
|
+
const name = names.get(slot);
|
|
173
|
+
if (name !== undefined && !firstSlotOfName.has(name)) {
|
|
174
|
+
firstSlotOfName.set(name, slot);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const assigned = new Map();
|
|
178
|
+
for (const node of slottables) {
|
|
179
|
+
const name = node.is(node.ELEMENT_NODE) ? getSlotName(node, 'slot') : '';
|
|
180
|
+
if (name === null) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const slot = firstSlotOfName.get(name) ?? null;
|
|
184
|
+
assigned.set(node, slot);
|
|
185
|
+
if (slot) {
|
|
186
|
+
slots.get(slot)?.push(node);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { slots, assigned };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The assignment of a host, computed once: the document does not change after it is built, as the other
|
|
193
|
+
* getters about trees (`MLElement#declarativeShadowRoot`) assume, so the answer never goes stale.
|
|
194
|
+
*
|
|
195
|
+
* @param host - A shadow host
|
|
196
|
+
* @returns The assignment, or `null` when it is not known
|
|
197
|
+
*/
|
|
198
|
+
function getSlotAssignment(
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
200
|
+
host) {
|
|
201
|
+
if (cache.has(host)) {
|
|
202
|
+
return cache.get(host) ?? null;
|
|
203
|
+
}
|
|
204
|
+
// The document does not change once it is built, as the other getters about trees assume
|
|
205
|
+
const assignment = compute(host);
|
|
206
|
+
cache.set(host, assignment);
|
|
207
|
+
return assignment;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The nodes a `slot` places.
|
|
211
|
+
*
|
|
212
|
+
* @template T - The rule configuration value type
|
|
213
|
+
* @template O - The rule options type
|
|
214
|
+
* @param slot - An element
|
|
215
|
+
* @returns The assigned nodes in tree order (`[]` when it takes none), or `null` when the element is not an
|
|
216
|
+
* HTML `slot` of a declarative shadow root whose assignment is known (see the module comment)
|
|
217
|
+
* @see https://dom.spec.whatwg.org/#dom-slot-assignednodes
|
|
218
|
+
*/
|
|
219
|
+
export function getAssignedNodes(
|
|
220
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
221
|
+
slot) {
|
|
222
|
+
if (!isHTMLSlot(slot)) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const root = slot.getRootNode();
|
|
226
|
+
if (!isShadowRoot(root)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return getSlotAssignment(root.host)?.slots.get(slot) ?? null;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The slot that places a node, as the flat tree has it.
|
|
233
|
+
*
|
|
234
|
+
* @template T - The rule configuration value type
|
|
235
|
+
* @template O - The rule options type
|
|
236
|
+
* @param node - An element or a text node
|
|
237
|
+
* @returns The slot; `null` when the node is a child of a shadow host that no slot of its shadow root
|
|
238
|
+
* takes, so that it is not rendered; `undefined` when the node is not a child of a shadow host, or the
|
|
239
|
+
* assignment is not known
|
|
240
|
+
* @see https://dom.spec.whatwg.org/#find-a-slot
|
|
241
|
+
*/
|
|
242
|
+
export function getFlatTreeSlot(
|
|
243
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
244
|
+
node) {
|
|
245
|
+
// The parent in the output: a block or a fragment that the engine renders away is not the parent
|
|
246
|
+
const host = getShadowHost(node);
|
|
247
|
+
if (!host?.declarativeShadowRoot) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
return getSlotAssignment(host)?.assigned.get(node);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The slot of an open shadow root that a node is assigned to.
|
|
254
|
+
*
|
|
255
|
+
* @template T - The rule configuration value type
|
|
256
|
+
* @template O - The rule options type
|
|
257
|
+
* @param node - An element or a text node
|
|
258
|
+
* @returns The slot, or `null` when none takes the node, its shadow root is closed, or the assignment is not known
|
|
259
|
+
* @see https://dom.spec.whatwg.org/#dom-slotable-assignedslot
|
|
260
|
+
*/
|
|
261
|
+
export function getAssignedSlot(
|
|
262
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
263
|
+
node) {
|
|
264
|
+
const slot = getFlatTreeSlot(node);
|
|
265
|
+
if (!slot) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const root = slot.getRootNode();
|
|
269
|
+
return 'mode' in root && root.mode === 'open' ? slot : null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The nodes a `slot` renders, with each assigned slot replaced by what that slot renders in turn, and the
|
|
273
|
+
* children of a slot that takes none in its place.
|
|
274
|
+
*
|
|
275
|
+
* @template T - The rule configuration value type
|
|
276
|
+
* @template O - The rule options type
|
|
277
|
+
* @param slot - An element
|
|
278
|
+
* @returns The nodes, or `[]` when the assignment is not known
|
|
279
|
+
* @see https://dom.spec.whatwg.org/#find-flattened-slotables
|
|
280
|
+
*/
|
|
281
|
+
export function getFlattenedSlottables(
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
283
|
+
slot) {
|
|
284
|
+
const assigned = getAssignedNodes(slot);
|
|
285
|
+
if (!assigned) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
const slottables = assigned.length > 0
|
|
289
|
+
? assigned
|
|
290
|
+
: [...slot.childNodes].filter(node => node.is(node.ELEMENT_NODE) || node.is(node.TEXT_NODE));
|
|
291
|
+
return slottables.flatMap(node => node.is(node.ELEMENT_NODE) && getAssignedNodes(node) !== null ? getFlattenedSlottables(node) : [node]);
|
|
292
|
+
}
|
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
import type { MLAttr } from '../node/attr.js';
|
|
2
2
|
import type { MLElement } from '../node/element.js';
|
|
3
|
+
import type { MLNode } from '../node/node.js';
|
|
3
4
|
import type { PlainData, RuleConfigValue } from '@markuplint/ml-config';
|
|
5
|
+
/**
|
|
6
|
+
* @module
|
|
7
|
+
*
|
|
8
|
+
* The tree of a node is what its `getRootNode()` returns: the document, the content of an
|
|
9
|
+
* HTML `template` element, or a declarative shadow root. An id is unique in a tree, and an id
|
|
10
|
+
* reference is resolved in a tree, as the specification says of "the element's tree".
|
|
11
|
+
*
|
|
12
|
+
* markuplint keeps the content of a `template` element as the children of the element (the
|
|
13
|
+
* syntax tree has no other place for it), so the boundary is made here and not by the shape of
|
|
14
|
+
* the tree: the queries of `MLParentNode`, `getRootNode()`, `treeParentElement`, the selector
|
|
15
|
+
* engine and `contains()` stop at it. `parentNode` / `parentElement` do not: the content
|
|
16
|
+
* model of a `template` element is validated against the elements that the syntax tree puts
|
|
17
|
+
* in it, so they go on to the `template` element. The algorithms of `@markuplint/ml-spec` walk up
|
|
18
|
+
* with `getTreeParentElement` / `getComposedParentElement`, which read `getRootNode()`, so the
|
|
19
|
+
* boundary made here is the boundary they stop at.
|
|
20
|
+
*
|
|
21
|
+
* @see https://dom.spec.whatwg.org/#concept-tree
|
|
22
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
|
|
23
|
+
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
24
|
+
*/
|
|
25
|
+
export declare const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
4
26
|
/**
|
|
5
27
|
* Whether a declarative shadow root is attached to the parent element of a `template` element.
|
|
6
28
|
*
|
|
7
29
|
* - `attached`: it is. The `template` element is in no tree, and its content is the shadow root.
|
|
8
30
|
* - `not-attached`: it is not. The `template` element is an ordinary one of the tree it is in.
|
|
9
31
|
* - `unknown`: the markup alone does not tell. The parent element is not in the file (the template is at
|
|
10
|
-
* the top of a partial), is a component,
|
|
32
|
+
* the top of a partial), is a component, is a pretended component that drops its children or puts
|
|
33
|
+
* them in a slot wrapper, or `shadowrootmode` is a dynamic value. markuplint does not
|
|
11
34
|
* decide what it cannot decide, as it does with a dynamic value elsewhere: the element is treated as
|
|
12
35
|
* in no tree, so its own id is not counted and the references of it are not reported.
|
|
13
36
|
*/
|
|
14
37
|
export type DeclarativeShadowRootAttachment = 'attached' | 'not-attached' | 'unknown';
|
|
38
|
+
/**
|
|
39
|
+
* Whether `node` is skipped when the parent of a `template` element is looked for: what the
|
|
40
|
+
* template engine or the framework renders away (a block of a template language, a fragment of
|
|
41
|
+
* JSX, a `template` with a directive such as `v-if`) leaves its children to the next element
|
|
42
|
+
* above.
|
|
43
|
+
*
|
|
44
|
+
* `isRenderedAway` of `@markuplint/rules` (`src/helpers.ts`) decides the same of an element for the
|
|
45
|
+
* rules that walk up the ancestors: a kind added here belongs to it too.
|
|
46
|
+
*
|
|
47
|
+
* @param node - A node of the syntax tree
|
|
48
|
+
* @returns `true` if the node is not an element of the output
|
|
49
|
+
*/
|
|
50
|
+
export declare function isTransparent<T extends RuleConfigValue, O extends PlainData>(node: MLNode<T, O>): boolean;
|
|
15
51
|
/**
|
|
16
52
|
* Whether the element is an HTML `template` element, which holds its children in `content`, a tree
|
|
17
53
|
* of its own.
|
|
@@ -37,14 +73,15 @@ export declare function isHTMLTemplateElement<T extends RuleConfigValue, O exten
|
|
|
37
73
|
export declare function getDeclarativeShadowRootMode<T extends RuleConfigValue, O extends PlainData>(template: MLElement<T, O>): 'open' | 'closed' | null;
|
|
38
74
|
/**
|
|
39
75
|
* The element that the HTML parser makes the shadow host of a `template` element: its parent in
|
|
40
|
-
* the output, where what the engine renders away (see {@link isTransparent}) is skipped.
|
|
76
|
+
* the output, where what the engine renders away (see {@link isTransparent}) is skipped. It is the
|
|
77
|
+
* parent in the output of any node, so it is the host a light child is a child of.
|
|
41
78
|
*
|
|
42
79
|
* @template T - The rule configuration value type
|
|
43
80
|
* @template O - The rule options type
|
|
44
|
-
* @param template - A `template` element
|
|
45
|
-
* @returns The element, or `null` when the file has none above the
|
|
81
|
+
* @param template - A `template` element, or any node whose parent in the output is wanted
|
|
82
|
+
* @returns The element, or `null` when the file has none above the node
|
|
46
83
|
*/
|
|
47
|
-
export declare function getShadowHost<T extends RuleConfigValue, O extends PlainData>(template:
|
|
84
|
+
export declare function getShadowHost<T extends RuleConfigValue, O extends PlainData>(template: MLNode<T, O>): MLElement<T, O> | null;
|
|
48
85
|
/**
|
|
49
86
|
* Whether a declarative shadow root is attached to the parent of a `template` element, by what
|
|
50
87
|
* the HTML parser does with the start tag of `template` and what the DOM does to attach a shadow
|
|
@@ -12,7 +12,7 @@ import { decodeEntities } from '@markuplint/shared';
|
|
|
12
12
|
* the tree: the queries of `MLParentNode`, `getRootNode()`, `treeParentElement`, the selector
|
|
13
13
|
* engine and `contains()` stop at it. `parentNode` / `parentElement` do not: the content
|
|
14
14
|
* model of a `template` element is validated against the elements that the syntax tree puts
|
|
15
|
-
* in it, so they
|
|
15
|
+
* in it, so they go on to the `template` element. The algorithms of `@markuplint/ml-spec` walk up
|
|
16
16
|
* with `getTreeParentElement` / `getComposedParentElement`, which read `getRootNode()`, so the
|
|
17
17
|
* boundary made here is the boundary they stop at.
|
|
18
18
|
*
|
|
@@ -20,7 +20,7 @@ import { decodeEntities } from '@markuplint/shared';
|
|
|
20
20
|
* @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
|
|
21
21
|
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
22
22
|
*/
|
|
23
|
-
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
23
|
+
export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
24
24
|
/**
|
|
25
25
|
* The names that the DOM accepts as a shadow host, besides a valid custom element name.
|
|
26
26
|
*
|
|
@@ -52,10 +52,13 @@ const SHADOW_HOST_NAMES = new Set([
|
|
|
52
52
|
* JSX, a `template` with a directive such as `v-if`) leaves its children to the next element
|
|
53
53
|
* above.
|
|
54
54
|
*
|
|
55
|
+
* `isRenderedAway` of `@markuplint/rules` (`src/helpers.ts`) decides the same of an element for the
|
|
56
|
+
* rules that walk up the ancestors: a kind added here belongs to it too.
|
|
57
|
+
*
|
|
55
58
|
* @param node - A node of the syntax tree
|
|
56
59
|
* @returns `true` if the node is not an element of the output
|
|
57
60
|
*/
|
|
58
|
-
function isTransparent(
|
|
61
|
+
export function isTransparent(
|
|
59
62
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
60
63
|
node) {
|
|
61
64
|
if (node.is(node.MARKUPLINT_PREPROCESSOR_BLOCK)) {
|
|
@@ -81,7 +84,8 @@ el) {
|
|
|
81
84
|
return false;
|
|
82
85
|
}
|
|
83
86
|
for (const attr of el.attributes) {
|
|
84
|
-
|
|
87
|
+
// A spread attribute (`{...props}`) sets attributes of the element, which stays
|
|
88
|
+
if (attr.isDirective && attr.localName !== '#spread') {
|
|
85
89
|
return true;
|
|
86
90
|
}
|
|
87
91
|
}
|
|
@@ -152,12 +156,13 @@ template) {
|
|
|
152
156
|
}
|
|
153
157
|
/**
|
|
154
158
|
* The element that the HTML parser makes the shadow host of a `template` element: its parent in
|
|
155
|
-
* the output, where what the engine renders away (see {@link isTransparent}) is skipped.
|
|
159
|
+
* the output, where what the engine renders away (see {@link isTransparent}) is skipped. It is the
|
|
160
|
+
* parent in the output of any node, so it is the host a light child is a child of.
|
|
156
161
|
*
|
|
157
162
|
* @template T - The rule configuration value type
|
|
158
163
|
* @template O - The rule options type
|
|
159
|
-
* @param template - A `template` element
|
|
160
|
-
* @returns The element, or `null` when the file has none above the
|
|
164
|
+
* @param template - A `template` element, or any node whose parent in the output is wanted
|
|
165
|
+
* @returns The element, or `null` when the file has none above the node
|
|
161
166
|
*/
|
|
162
167
|
export function getShadowHost(
|
|
163
168
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -194,6 +199,28 @@ host) {
|
|
|
194
199
|
const name = host.localName;
|
|
195
200
|
return SHADOW_HOST_NAMES.has(name) || isPotentialCustomElementName(name);
|
|
196
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether the children written in `host` are its children in the output, so that a `template`
|
|
204
|
+
* among them is parsed in it. A pretended component renders them in the element it pretends to
|
|
205
|
+
* be only when it neither drops them (`slots: null`) nor puts them in a slot wrapper; a custom
|
|
206
|
+
* element keeps them in its light DOM. This is the same placement as the rendered parent
|
|
207
|
+
* `MLElement` gives a child for its namespace.
|
|
208
|
+
*
|
|
209
|
+
* @param host - The parent element of a `template` element
|
|
210
|
+
* @returns `false` when the component places the children elsewhere or not at all
|
|
211
|
+
*/
|
|
212
|
+
function rendersChildrenInItself(
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
214
|
+
host) {
|
|
215
|
+
const context = host.pretenderContext;
|
|
216
|
+
if (context?.type !== 'pretender' || host.elementType === 'web-component') {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (context.as.childNodes.length === 0) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return !context.slotContent || context.slotContent.wrapper === context.as;
|
|
223
|
+
}
|
|
197
224
|
/**
|
|
198
225
|
* The `template` elements that sit in the output under the host, in the order of the markup,
|
|
199
226
|
* seen through what the engine renders away.
|
|
@@ -248,7 +275,7 @@ template) {
|
|
|
248
275
|
return 'not-attached';
|
|
249
276
|
}
|
|
250
277
|
const host = getShadowHost(template);
|
|
251
|
-
if (!host) {
|
|
278
|
+
if (!host || !rendersChildrenInItself(host)) {
|
|
252
279
|
return 'unknown';
|
|
253
280
|
}
|
|
254
281
|
const hostCheck = canBeShadowHost(host);
|
|
@@ -50,6 +50,11 @@ export declare class MLDocument<T extends RuleConfigValue, O extends PlainData =
|
|
|
50
50
|
* this.
|
|
51
51
|
*/
|
|
52
52
|
readonly literalValues?: LiteralValueDeclaration;
|
|
53
|
+
/**
|
|
54
|
+
* What the `slot` element is in the language of the document (`MLParser#slotElement`): the HTML
|
|
55
|
+
* element, or the placeholder of a framework's component, whose slot assignment is not computed.
|
|
56
|
+
*/
|
|
57
|
+
readonly slotElement: 'html' | 'framework';
|
|
53
58
|
/**
|
|
54
59
|
* The rule currently being evaluated during a lint pass.
|
|
55
60
|
* Set by the linting engine before rule evaluation begins and used by nodes
|
|
@@ -151,6 +156,7 @@ export declare class MLDocument<T extends RuleConfigValue, O extends PlainData =
|
|
|
151
156
|
readonly booleanish?: boolean;
|
|
152
157
|
readonly attrExpressionLanguage?: ExpressionLanguage;
|
|
153
158
|
readonly literalValues?: LiteralValueDeclaration;
|
|
159
|
+
readonly slotElement?: 'html' | 'framework';
|
|
154
160
|
readonly tagNameCaseSensitive?: boolean;
|
|
155
161
|
readonly componentNameVariants?: boolean;
|
|
156
162
|
readonly isComponentName?: (name: string) => boolean;
|
|
@@ -1784,12 +1790,16 @@ export declare class MLDocument<T extends RuleConfigValue, O extends PlainData =
|
|
|
1784
1790
|
/**
|
|
1785
1791
|
* Searches for a node at the given source location (line and column).
|
|
1786
1792
|
*
|
|
1793
|
+
* The node is the innermost one whose source range, from its start, included,
|
|
1794
|
+
* to its end, excluded, holds the position; a position in an end tag is its
|
|
1795
|
+
* element. The rule is `searchNodeAtLocation`'s.
|
|
1796
|
+
*
|
|
1787
1797
|
* @implements `@markuplint/ml-core` API: `MLDocument`
|
|
1788
1798
|
* @param line - The 1-based line number to search at
|
|
1789
1799
|
* @param col - The 1-based column number to search at
|
|
1790
1800
|
* @returns The node at the given location, or null if not found
|
|
1791
1801
|
*/
|
|
1792
|
-
searchNodeByLocation(line: number, col: number): MLNode<T, O
|
|
1802
|
+
searchNodeByLocation(line: number, col: number): MLNode<T, O> | null;
|
|
1793
1803
|
/**
|
|
1794
1804
|
* Sets the currently active rule for this document. Called by the linting engine
|
|
1795
1805
|
* before each rule evaluation pass.
|