@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 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. Assigned nodes are not computed
38
- * (`MLElement#assignedNodes` returns none), so a slot contributes its own children.
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
- * A pretended component is unchanged: `as.childNodes` is empty when it never
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. Assigned nodes are not computed
119
- * (`MLElement#assignedNodes` returns none), so a slot contributes its own children.
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
- * A pretended component is unchanged: `as.childNodes` is empty when it never
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, or `shadowrootmode` is a dynamic value. markuplint does not
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 template
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: MLElement<T, O>): MLElement<T, O> | null;
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 stay the syntactical ones. The algorithms of `@markuplint/ml-spec` walk up
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
- if (attr.isDirective) {
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 template
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, import("@markuplint/ml-ast").MLASTNode> | null;
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.