@markuplint/ml-core 5.4.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/lib/ml-core.js +3 -0
  3. package/lib/ml-dom/helper/accname.d.ts +35 -0
  4. package/lib/ml-dom/helper/accname.js +69 -17
  5. package/lib/ml-dom/helper/debug.js +1 -1
  6. package/lib/ml-dom/helper/literal-values.d.ts +102 -0
  7. package/lib/ml-dom/helper/literal-values.js +193 -0
  8. package/lib/ml-dom/helper/pretended-attrs.d.ts +16 -0
  9. package/lib/ml-dom/helper/pretended-attrs.js +21 -0
  10. package/lib/ml-dom/helper/slot-assignment.d.ts +48 -0
  11. package/lib/ml-dom/helper/slot-assignment.js +292 -0
  12. package/lib/ml-dom/helper/tree-scope.d.ts +130 -0
  13. package/lib/ml-dom/helper/tree-scope.js +341 -0
  14. package/lib/ml-dom/helper/walkers.d.ts +15 -0
  15. package/lib/ml-dom/helper/walkers.js +34 -0
  16. package/lib/ml-dom/index.d.ts +4 -0
  17. package/lib/ml-dom/index.js +2 -0
  18. package/lib/ml-dom/node/attr.d.ts +77 -10
  19. package/lib/ml-dom/node/attr.js +209 -20
  20. package/lib/ml-dom/node/block.d.ts +15 -6
  21. package/lib/ml-dom/node/block.js +17 -8
  22. package/lib/ml-dom/node/document-fragment.d.ts +18 -6
  23. package/lib/ml-dom/node/document-fragment.js +19 -7
  24. package/lib/ml-dom/node/document.d.ts +36 -7
  25. package/lib/ml-dom/node/document.js +60 -17
  26. package/lib/ml-dom/node/dom-token-list.js +28 -3
  27. package/lib/ml-dom/node/element.d.ts +148 -24
  28. package/lib/ml-dom/node/element.js +258 -48
  29. package/lib/ml-dom/node/named-node-map.js +2 -1
  30. package/lib/ml-dom/node/node-list.js +2 -3
  31. package/lib/ml-dom/node/node.d.ts +55 -10
  32. package/lib/ml-dom/node/node.js +98 -13
  33. package/lib/ml-dom/node/parent-node.d.ts +39 -8
  34. package/lib/ml-dom/node/parent-node.js +79 -12
  35. package/lib/ml-dom/node/shadow-root.d.ts +162 -0
  36. package/lib/ml-dom/node/shadow-root.js +219 -0
  37. package/lib/ml-dom/node/text.d.ts +12 -3
  38. package/lib/ml-dom/node/text.js +14 -3
  39. package/lib/ml-rule/ml-rule.js +4 -1
  40. package/lib/test/index.js +3 -0
  41. package/lib/utils/index.d.ts +2 -0
  42. package/lib/utils/index.js +1 -0
  43. package/lib/utils/search-node-at-location.d.ts +39 -0
  44. package/lib/utils/search-node-at-location.js +53 -0
  45. package/package.json +12 -15
@@ -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
+ }
@@ -0,0 +1,130 @@
1
+ import type { MLAttr } from '../node/attr.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
+ * @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";
26
+ /**
27
+ * Whether a declarative shadow root is attached to the parent element of a `template` element.
28
+ *
29
+ * - `attached`: it is. The `template` element is in no tree, and its content is the shadow root.
30
+ * - `not-attached`: it is not. The `template` element is an ordinary one of the tree it is in.
31
+ * - `unknown`: the markup alone does not tell. The parent element is not in the file (the template is at
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
34
+ * decide what it cannot decide, as it does with a dynamic value elsewhere: the element is treated as
35
+ * in no tree, so its own id is not counted and the references of it are not reported.
36
+ */
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;
51
+ /**
52
+ * Whether the element is an HTML `template` element, which holds its children in `content`, a tree
53
+ * of its own.
54
+ *
55
+ * It is not when the element is a fragment, has a directive of a template engine, or the
56
+ * framework puts the children in the element itself (`templateContent: 'in-place'` of the spec).
57
+ *
58
+ * @template T - The rule configuration value type
59
+ * @template O - The rule options type
60
+ * @param el - An element
61
+ * @returns `true` if the children of the element belong to a tree of their own
62
+ */
63
+ export declare function isHTMLTemplateElement<T extends RuleConfigValue, O extends PlainData>(el: MLElement<T, O>): boolean;
64
+ /**
65
+ * The mode of the declarative shadow root that a `template` element asks for.
66
+ *
67
+ * @template T - The rule configuration value type
68
+ * @template O - The rule options type
69
+ * @param template - An element
70
+ * @returns `open` or `closed`, or `null` when it is not an HTML `template` element, the
71
+ * attribute is missing or invalid, or its value is dynamic
72
+ */
73
+ export declare function getDeclarativeShadowRootMode<T extends RuleConfigValue, O extends PlainData>(template: MLElement<T, O>): 'open' | 'closed' | null;
74
+ /**
75
+ * The element that the HTML parser makes the shadow host of a `template` element: its parent in
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.
78
+ *
79
+ * @template T - The rule configuration value type
80
+ * @template O - The rule options type
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
83
+ */
84
+ export declare function getShadowHost<T extends RuleConfigValue, O extends PlainData>(template: MLNode<T, O>): MLElement<T, O> | null;
85
+ /**
86
+ * Whether a declarative shadow root is attached to the parent of a `template` element, by what
87
+ * the HTML parser does with the start tag of `template` and what the DOM does to attach a shadow
88
+ * root: the mode is `open` or `closed` (ASCII case-insensitive), the parent is an HTML element with
89
+ * a valid shadow host name (a valid custom element name, or one of the names the DOM lists), and no
90
+ * earlier `template` of the same parent has attached one.
91
+ *
92
+ * The file is taken as parsed as a document that was served, so declarative shadow roots are
93
+ * allowed. The parser would not attach one to the top level of a fragment parsed through `innerHTML`,
94
+ * but whether a file is that is not something its markup says.
95
+ *
96
+ * Known limitation: a custom element definition may disable the shadow root (`disable shadow`),
97
+ * which only the script that defines the element tells. It is taken as not disabled.
98
+ *
99
+ * @template T - The rule configuration value type
100
+ * @template O - The rule options type
101
+ * @param template - An element
102
+ * @returns The state. An element that is not an HTML `template` element is `not-attached`
103
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhead
104
+ * @see https://dom.spec.whatwg.org/#concept-attach-a-shadow-root
105
+ */
106
+ export declare function getDeclarativeShadowRootAttachment<T extends RuleConfigValue, O extends PlainData>(template: MLElement<T, O>): DeclarativeShadowRootAttachment;
107
+ /**
108
+ * The `template` element of a host whose declarative shadow root is attached, if there is one.
109
+ *
110
+ * @template T - The rule configuration value type
111
+ * @template O - The rule options type
112
+ * @param host - An element
113
+ * @returns The `template` element whose content is the shadow root of the host, or `null`
114
+ */
115
+ export declare function getShadowRootTemplate<T extends RuleConfigValue, O extends PlainData>(host: MLElement<T, O>): MLElement<T, O> | null;
116
+ /**
117
+ * The string that the DOM has for an attribute: the value with the character references in the markup
118
+ * resolved, so that `a&#x62;` is `ab`. A value that is written as code is a string already, with no
119
+ * character reference in it.
120
+ *
121
+ * @template T - The rule configuration value type
122
+ * @template O - The rule options type
123
+ * @param attr - An attribute, or `null` for one that is missing
124
+ * @param value - A part of the value of the attribute as it is written (one token of a list), which the
125
+ * attribute decides on whether it has a character reference; the whole value by default
126
+ * @returns The string, or `null` when the attribute is missing, a directive, or its value is not known
127
+ * until it is rendered (a dynamic value)
128
+ * @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
129
+ */
130
+ export declare function getDOMValue<T extends RuleConfigValue, O extends PlainData>(attr: MLAttr<T, O> | null | undefined, value?: string): string | null;