@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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,35 @@
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
+
21
+ # [5.5.0](https://github.com/markuplint/markuplint/compare/v5.4.0...v5.5.0) (2026-11-10)
22
+
23
+ ### Bug Fixes
24
+
25
+ * **ml-core:** cite the DOM, CSSOM View and DOM Parsing members at their current anchors ([809f9f0](https://github.com/markuplint/markuplint/commit/809f9f0ae61adca02c9b51beaf7660706f1eaf02))
26
+ * **ml-core:** omit unrendered template content from accessible names ([55688bc](https://github.com/markuplint/markuplint/commit/55688bc6a7b5860de775760fae1fdf9ba6bd94e6))
27
+ * **ml-core:** return the last element child from lastElementChild ([f58cc9f](https://github.com/markuplint/markuplint/commit/f58cc9f2ae9d438ae2235ff6745c00d8a4be517f)), closes [#4503](https://github.com/markuplint/markuplint/issues/4503)
28
+
29
+ ### Features
30
+
31
+ * **ml-core:** make the DOM tree-aware: template content and declarative shadow roots are trees ([651852d](https://github.com/markuplint/markuplint/commit/651852da83052f7a51aab5f2dd06a4f18d19d36a))
32
+ * **ml-core:** render a literal in an attribute value as the framework renders it ([f6ff61d](https://github.com/markuplint/markuplint/commit/f6ff61d91d180937db972f908e81d796ca9a4774))
33
+
34
+
6
35
  # [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
7
36
 
8
37
  ### Features
package/lib/ml-core.js CHANGED
@@ -344,6 +344,9 @@ export class MLCore {
344
344
  filename: this.#filename,
345
345
  endTag: this.#parser.endTag,
346
346
  booleanish: this.#parser.booleanish,
347
+ attrExpressionLanguage: this.#parser.attrExpressionLanguage,
348
+ literalValues: this.#parser.literalValues,
349
+ slotElement: this.#parser.slotElement,
347
350
  tagNameCaseSensitive: this.#parser.tagNameCaseSensitive,
348
351
  componentNameVariants: this.#parser.componentNameVariants,
349
352
  // The parser's own rule, called on the parser: a method of a class that reads its own state
@@ -1,4 +1,6 @@
1
1
  import type { MLElement } from '../node/element.js';
2
+ import type { MLNode } from '../node/node.js';
3
+ import type { PlainData, RuleConfigValue } from '@markuplint/ml-config';
2
4
  import type { AccnameSource, AccnameVisit, ARIAVersion } from '@markuplint/ml-spec';
3
5
  export declare function getAccname(el: MLElement<any, any>, version: ARIAVersion): string;
4
6
  /**
@@ -25,3 +27,36 @@ export declare function getReferencedTextAlternative(el: MLElement<any, any>, ve
25
27
  * handled as in {@link getAccname}: the element is taken to have none.
26
28
  */
27
29
  export declare function getHostLanguageLabelSource(el: MLElement<any, any>, version: ARIAVersion): AccnameSource | null;
30
+ /**
31
+ * The children the accessible-name computation renders for `el`.
32
+ *
33
+ * An ordinary `template` element contributes none: its content is another tree
34
+ * and is not rendered where the element is written. A host renders the child
35
+ * nodes of its declarative shadow root, of either mode. A `template` whose
36
+ * shadow root is only maybe attached contributes none too, because that root
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
+ *
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
44
+ * renders its children (`slots: null`), so text written inside `<Img>Save</Img>`
45
+ * does not become its name. `contents` may wrap the children given at the usage site.
46
+ *
47
+ * > The template contents of a template element are not children of the element itself.
48
+ *
49
+ * > If the current node has an attached shadow root, set the rendered child nodes to be the child nodes of the shadow root.
50
+ *
51
+ * > Otherwise, if the current node is a slot with assigned nodes, set the rendered child nodes to be the assigned nodes of the current node.
52
+ *
53
+ * > Otherwise, set the rendered child nodes to be the child nodes of the current node.
54
+ *
55
+ * @template T - The rule configuration value type
56
+ * @template O - The rule options type
57
+ * @param el - The element whose rendered children are asked for
58
+ * @returns The nodes the computation walks
59
+ * @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
60
+ * @see https://w3c.github.io/accname/#comp_name_from_content
61
+ */
62
+ export declare function getRenderedChildNodes<T extends RuleConfigValue, O extends PlainData>(el: MLElement<T, O>): Iterable<MLNode<T, O>>;
@@ -1,5 +1,7 @@
1
- import { computeAccessibleName, computeHostLanguageLabel, computeReferencedTextAlternative, escapeCSS, getComputedRole, EMBEDDED_CONTROL_ROLES, isExposedAsPresentational, isNativeEmbeddedControl, } from '@markuplint/ml-spec';
1
+ import { computeAccessibleName, computeHostLanguageLabel, computeReferencedTextAlternative, getComputedRole, EMBEDDED_CONTROL_ROLES, isExposedAsPresentational, isNativeEmbeddedControl, } from '@markuplint/ml-spec';
2
+ import { decodeEntities } from '@markuplint/shared';
2
3
  import { log } from '../../debug.js';
4
+ import { getDOMValue } from './tree-scope.js';
3
5
  const accnameLog = log.extend('accname');
4
6
  export function getAccname(
5
7
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -106,18 +108,79 @@ el, error) {
106
108
  accnameLog('Error: %O', error);
107
109
  }
108
110
  }
111
+ /**
112
+ * The children the accessible-name computation renders for `el`.
113
+ *
114
+ * An ordinary `template` element contributes none: its content is another tree
115
+ * and is not rendered where the element is written. A host renders the child
116
+ * nodes of its declarative shadow root, of either mode. A `template` whose
117
+ * shadow root is only maybe attached contributes none too, because that root
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
+ *
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
125
+ * renders its children (`slots: null`), so text written inside `<Img>Save</Img>`
126
+ * does not become its name. `contents` may wrap the children given at the usage site.
127
+ *
128
+ * > The template contents of a template element are not children of the element itself.
129
+ *
130
+ * > If the current node has an attached shadow root, set the rendered child nodes to be the child nodes of the shadow root.
131
+ *
132
+ * > Otherwise, if the current node is a slot with assigned nodes, set the rendered child nodes to be the assigned nodes of the current node.
133
+ *
134
+ * > Otherwise, set the rendered child nodes to be the child nodes of the current node.
135
+ *
136
+ * @template T - The rule configuration value type
137
+ * @template O - The rule options type
138
+ * @param el - The element whose rendered children are asked for
139
+ * @returns The nodes the computation walks
140
+ * @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
141
+ * @see https://w3c.github.io/accname/#comp_name_from_content
142
+ */
143
+ export function getRenderedChildNodes(
144
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
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
+ }
154
+ const context = el.pretenderContext;
155
+ if (context?.type === 'pretender') {
156
+ const given = [...context.as.childNodes];
157
+ return context.slotContent ? context.slotContent.fill(given) : given;
158
+ }
159
+ if (el.isHTMLTemplateElement) {
160
+ return [];
161
+ }
162
+ return el.childNodes;
163
+ }
109
164
  function createMLCoreResolver(
110
165
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
111
166
  el, version) {
112
167
  const doc = el.ownerMLDocument;
168
+ // An id is resolved in the tree of the element: not in the content of a `template` outside of it.
169
+ // An element that is in no tree (a `template` that a declarative shadow root is attached to) is its
170
+ // own root and resolves nothing.
171
+ const node = el.getRootNode();
172
+ const root = node.is(node.DOCUMENT_NODE) || node.is(node.DOCUMENT_FRAGMENT_NODE) ? node : null;
173
+ // The ids the algorithm hands over are the ones written in the markup (`MLElement#id` and a token of
174
+ // `aria-labelledby`), so the character references are resolved on both sides, as the DOM has them
113
175
  return {
114
176
  getElementById(id) {
115
- const found = doc.querySelector(`#${escapeCSS(id)}`);
116
- return found ?? null;
177
+ return root?.getElementById(decodeEntities(id)) ?? null;
117
178
  },
118
179
  getLabelsForId(id) {
119
- const labels = doc.querySelectorAll(`label[for="${escapeCSS(id)}"]`);
120
- return [...labels];
180
+ const domId = decodeEntities(id);
181
+ return root
182
+ ? [...root.querySelectorAll('label[for]')].filter(label => getDOMValue(label.getAttributeNode('for')) === domId)
183
+ : [];
121
184
  },
122
185
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
123
186
  allowsNameFromContent(target) {
@@ -164,18 +227,7 @@ el, version) {
164
227
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
165
228
  getChildNodes(target) {
166
229
  // TODO: Remove cast when AccnameResolver is generic (#3178)
167
- const targetEl = target;
168
- const context = targetEl.pretenderContext;
169
- if (context?.type !== 'pretender') {
170
- return targetEl.childNodes;
171
- }
172
- // The children the component renders: `as.childNodes` is empty when it never
173
- // renders its children (`slots: null`), so text written inside `<Img>Save</Img>`
174
- // does not become its name.
175
- const given = [...context.as.childNodes];
176
- // A pretended component renders its own elements (`contents`) around the
177
- // children given at the usage site; the name can come from either.
178
- return context.slotContent ? context.slotContent.fill(given) : given;
230
+ return getRenderedChildNodes(target);
179
231
  },
180
232
  };
181
233
  }
@@ -64,7 +64,7 @@ attributes) {
64
64
  if (n.spacesBeforeName) {
65
65
  r.push(
66
66
  //
67
- ` isDirective: ${!!n.isDirective}`, ` isDynamicValue: ${!!n.isDynamicValue}`);
67
+ ` isDirective: ${!!n.isDirective}`, ` isExpressionValue: ${!!n.isExpressionValue}`, ` isDynamicValue: ${!!n.isDynamicValue}`, ` isAbsent: ${!!n.isAbsent}`);
68
68
  }
69
69
  if (n.candidate) {
70
70
  r.push(` candidate: ${visibleWhiteSpace(n.candidate)}`);
@@ -0,0 +1,102 @@
1
+ import type { LiteralValueDeclaration } from '@markuplint/ml-ast';
2
+ import type { MLMLSpec } from '@markuplint/ml-spec';
3
+ import type { LiteralExpression } from '@markuplint/parser-utils';
4
+ /**
5
+ * How a literal in an attribute value that is written as code is rendered as an attribute of the DOM.
6
+ *
7
+ * - `value`: the attribute has this value (`""` for `disabled={true}`)
8
+ * - `absent`: the attribute is not output (`disabled={false}`), as `getAttribute()` returns `null`
9
+ * - `unknown`: the frameworks differ and the attribute has a value that is not known (`disabled={""}`
10
+ * is `""` in Vue and Svelte, and absent in React and Astro)
11
+ */
12
+ export type RenderedLiteral = {
13
+ readonly kind: 'value';
14
+ readonly value: string;
15
+ } | {
16
+ readonly kind: 'absent';
17
+ } | {
18
+ readonly kind: 'unknown';
19
+ };
20
+ /**
21
+ * What decides the rendering of a literal besides the literal itself.
22
+ */
23
+ export type LiteralRenderingContext = {
24
+ /**
25
+ * The name of the attribute as the DOM names it, in lower case.
26
+ */
27
+ readonly name: string;
28
+ /**
29
+ * Whether the attribute is a boolean attribute: a name whose type is `Boolean` in the spec
30
+ * ({@link getBooleanAttrNames}). The frameworks decide it by the name, not by the element.
31
+ */
32
+ readonly isBoolean: boolean;
33
+ /**
34
+ * Whether the element is a custom element (`elementType: "web-component"`), which React and Astro
35
+ * render differently from an element of HTML.
36
+ */
37
+ readonly isCustomElement: boolean;
38
+ /**
39
+ * `literalValues` of the merged spec (`MLMLSpec#literalValues`).
40
+ */
41
+ readonly specLiteralValues?: LiteralValueDeclaration;
42
+ /**
43
+ * `literalValues` that the parser declares (`MLParser#literalValues`), which a parser without a spec
44
+ * package has. A spec wins over it on a key that both say.
45
+ */
46
+ readonly parserLiteralValues?: LiteralValueDeclaration;
47
+ };
48
+ /**
49
+ * Renders a literal in an attribute value that is written as code, as the framework renders it as
50
+ * an attribute of the DOM.
51
+ *
52
+ * The tables are looked up by the key of the literal (the normal form in `LiteralExpression#key`),
53
+ * and the first table that has the key decides, in this order. A spec is read before a parser at each step.
54
+ *
55
+ * 1. On a custom element, `customElements`: the table of the attribute by name (`attrs`), then that of
56
+ * its kind
57
+ * 2. `attrs`: the table of the attribute by name
58
+ * 3. The table of `@markuplint/ml-core` by name ({@link NAME_DEFAULTS})
59
+ * 4. The table of the kind of the attribute (`ariaAttrs` for `aria-`, `dataAttrs` for `data-`,
60
+ * `booleanAttrs` for a boolean attribute, `otherAttrs` for the rest). The kinds are independent: an
61
+ * attribute is of one kind and does not fall back to another
62
+ * 5. The default of `@markuplint/ml-core` ({@link renderByDefault})
63
+ *
64
+ * A table lists only what differs from the default, so most attributes are rendered by 5.
65
+ *
66
+ * @param literal - The literal that is written
67
+ * @param context - What decides the rendering besides the literal
68
+ * @returns How the attribute is rendered
69
+ */
70
+ export declare function renderLiteral(literal: LiteralExpression, context: LiteralRenderingContext): RenderedLiteral;
71
+ /**
72
+ * The string of a literal as it is written, which is what a component receives as a prop of the
73
+ * literal. A component does not render it: where it ends up is decided by the element that the
74
+ * prop is forwarded to.
75
+ *
76
+ * @param literal - The literal that is written
77
+ * @returns The string form: a string as it is, a number by `String(n)`, a boolean as `"true"` or
78
+ * `"false"`, and `null` or `undefined` as the empty string
79
+ */
80
+ export declare function literalToString(literal: LiteralExpression): string;
81
+ /**
82
+ * The code that reads as the literal again: the key of the literal, except for a number that is not
83
+ * finite, whose key (`Infinity`) is a name and not a literal, so it is written as the number that overflows.
84
+ *
85
+ * @param literal - The literal that is written
86
+ * @returns The code that `parseLiteralExpression` reads as the same literal
87
+ */
88
+ export declare function literalToExpression(literal: LiteralExpression): string;
89
+ /**
90
+ * The names of the boolean attributes of a spec: those whose type is `Boolean` somewhere in it, in
91
+ * the global attributes or in an element, so `open` is one though it is another type on `<mfenced>`.
92
+ * An attribute with several types, or a conditional type, counts when any of them is `Boolean`.
93
+ * Names are in lower case.
94
+ *
95
+ * The frameworks decide a boolean attribute by its name alone (React, Vue, Svelte, Astro and Alpine
96
+ * each have a list of names), so the element does not matter. `hidden` is not in the set: its type is
97
+ * enumerated in the HTML Standard.
98
+ *
99
+ * @param spec - The merged spec
100
+ * @returns The set of names, computed once for a spec
101
+ */
102
+ export declare function getBooleanAttrNames(spec: MLMLSpec): ReadonlySet<string>;
@@ -0,0 +1,193 @@
1
+ import { asciiLowerCase } from '@markuplint/shared';
2
+ /**
3
+ * The tables of `@markuplint/ml-core` by attribute name, for an attribute whose rendering the
4
+ * frameworks agree on and the spec cannot say: `hidden` is an enumerated attribute (`""`, `hidden`,
5
+ * `until-found`) in the HTML Standard, but React, Vue, Svelte and Astro all render a boolean as
6
+ * present or absent. What differs among them is said by `literalValues` of the spec or the parser.
7
+ *
8
+ * It lists what the frameworks agree on. A number other than `0` is a string by the default (`hidden={1}` is
9
+ * `hidden="1"`, which React, Vue and Svelte render as `hidden=""`): the attribute is output either way. The
10
+ * empty string is present by the default; React does not output it, which its spec says.
11
+ *
12
+ * It is read after `attrs` of a spec or a parser, so that a framework that renders `hidden` differently
13
+ * can say so there, and before the table of its kind (`otherAttrs`, as `hidden` is not `Boolean` in the
14
+ * spec), so that what a framework says for the other attributes does not change it.
15
+ */
16
+ const NAME_DEFAULTS = {
17
+ hidden: { true: '', false: null, '0': null },
18
+ };
19
+ /**
20
+ * Renders a literal in an attribute value that is written as code, as the framework renders it as
21
+ * an attribute of the DOM.
22
+ *
23
+ * The tables are looked up by the key of the literal (the normal form in `LiteralExpression#key`),
24
+ * and the first table that has the key decides, in this order. A spec is read before a parser at each step.
25
+ *
26
+ * 1. On a custom element, `customElements`: the table of the attribute by name (`attrs`), then that of
27
+ * its kind
28
+ * 2. `attrs`: the table of the attribute by name
29
+ * 3. The table of `@markuplint/ml-core` by name ({@link NAME_DEFAULTS})
30
+ * 4. The table of the kind of the attribute (`ariaAttrs` for `aria-`, `dataAttrs` for `data-`,
31
+ * `booleanAttrs` for a boolean attribute, `otherAttrs` for the rest). The kinds are independent: an
32
+ * attribute is of one kind and does not fall back to another
33
+ * 5. The default of `@markuplint/ml-core` ({@link renderByDefault})
34
+ *
35
+ * A table lists only what differs from the default, so most attributes are rendered by 5.
36
+ *
37
+ * @param literal - The literal that is written
38
+ * @param context - What decides the rendering besides the literal
39
+ * @returns How the attribute is rendered
40
+ */
41
+ export function renderLiteral(literal, context) {
42
+ const { key } = literal;
43
+ const { name, specLiteralValues: spec, parserLiteralValues: parser } = context;
44
+ const kind = kindOf(name, context.isBoolean);
45
+ const tables = [];
46
+ if (context.isCustomElement) {
47
+ tables.push(spec?.customElements?.attrs?.[name], parser?.customElements?.attrs?.[name], spec?.customElements?.[kind], parser?.customElements?.[kind]);
48
+ }
49
+ tables.push(spec?.attrs?.[name], parser?.attrs?.[name], NAME_DEFAULTS[name], spec?.[kind], parser?.[kind]);
50
+ for (const table of tables) {
51
+ if (table && Object.hasOwn(table, key)) {
52
+ const rendered = table[key];
53
+ return rendered == null ? { kind: 'absent' } : { kind: 'value', value: rendered };
54
+ }
55
+ }
56
+ return renderByDefault(literal, kind);
57
+ }
58
+ function kindOf(name, isBoolean) {
59
+ if (name.startsWith('aria-')) {
60
+ return 'ariaAttrs';
61
+ }
62
+ if (name.startsWith('data-')) {
63
+ return 'dataAttrs';
64
+ }
65
+ return isBoolean ? 'booleanAttrs' : 'otherAttrs';
66
+ }
67
+ /**
68
+ * The rendering that React, Vue, Svelte and Astro share, which a table of a spec or a parser
69
+ * overrides on the keys it lists.
70
+ *
71
+ * | literal | boolean attribute | others |
72
+ * | ------------------ | ------------------------- | --------------- |
73
+ * | `true` | `""` | `"true"` |
74
+ * | `false` | not output | `"false"` |
75
+ * | `null`/`undefined` | not output | not output |
76
+ * | a number except 0 | `""` | `String(n)` |
77
+ * | `0` | not output | `"0"` |
78
+ * | `""` | unknown | `""` |
79
+ * | any other string | `""` | the string |
80
+ *
81
+ * `aria-` and `data-` attributes are rendered as the others. The empty string on a boolean attribute is
82
+ * the one that the frameworks disagree on (present in Vue and Svelte, absent in React and Astro), so it is
83
+ * unknown unless the spec or the parser says. Svelte renders any other string on a boolean attribute
84
+ * as it is when it is rendered on the server, which is not told from `""` here: both mean that the
85
+ * attribute is present.
86
+ */
87
+ function renderByDefault(literal, kind) {
88
+ const { type, value } = literal;
89
+ if (type === 'null' || type === 'undefined') {
90
+ return { kind: 'absent' };
91
+ }
92
+ if (kind === 'booleanAttrs') {
93
+ switch (type) {
94
+ case 'boolean': {
95
+ return value ? { kind: 'value', value: '' } : { kind: 'absent' };
96
+ }
97
+ case 'number': {
98
+ return value === 0 ? { kind: 'absent' } : { kind: 'value', value: '' };
99
+ }
100
+ default: {
101
+ return value === '' ? { kind: 'unknown' } : { kind: 'value', value: '' };
102
+ }
103
+ }
104
+ }
105
+ // The key of a number is its digits: a BigInt keeps all of them
106
+ return { kind: 'value', value: type === 'number' ? literal.key : String(value) };
107
+ }
108
+ /**
109
+ * The string of a literal as it is written, which is what a component receives as a prop of the
110
+ * literal. A component does not render it: where it ends up is decided by the element that the
111
+ * prop is forwarded to.
112
+ *
113
+ * @param literal - The literal that is written
114
+ * @returns The string form: a string as it is, a number by `String(n)`, a boolean as `"true"` or
115
+ * `"false"`, and `null` or `undefined` as the empty string
116
+ */
117
+ export function literalToString(literal) {
118
+ switch (literal.type) {
119
+ case 'null':
120
+ case 'undefined': {
121
+ return '';
122
+ }
123
+ case 'number': {
124
+ return literal.key;
125
+ }
126
+ default: {
127
+ return String(literal.value);
128
+ }
129
+ }
130
+ }
131
+ /**
132
+ * The code that reads as the literal again: the key of the literal, except for a number that is not
133
+ * finite, whose key (`Infinity`) is a name and not a literal, so it is written as the number that overflows.
134
+ *
135
+ * @param literal - The literal that is written
136
+ * @returns The code that `parseLiteralExpression` reads as the same literal
137
+ */
138
+ export function literalToExpression(literal) {
139
+ if (literal.type === 'number' && !Number.isFinite(literal.value)) {
140
+ return literal.value > 0 ? '1e999' : '-1e999';
141
+ }
142
+ return literal.key;
143
+ }
144
+ const booleanAttrNamesCache = new WeakMap();
145
+ /**
146
+ * The names of the boolean attributes of a spec: those whose type is `Boolean` somewhere in it, in
147
+ * the global attributes or in an element, so `open` is one though it is another type on `<mfenced>`.
148
+ * An attribute with several types, or a conditional type, counts when any of them is `Boolean`.
149
+ * Names are in lower case.
150
+ *
151
+ * The frameworks decide a boolean attribute by its name alone (React, Vue, Svelte, Astro and Alpine
152
+ * each have a list of names), so the element does not matter. `hidden` is not in the set: its type is
153
+ * enumerated in the HTML Standard.
154
+ *
155
+ * @param spec - The merged spec
156
+ * @returns The set of names, computed once for a spec
157
+ */
158
+ export function getBooleanAttrNames(spec) {
159
+ let names = booleanAttrNamesCache.get(spec);
160
+ if (names) {
161
+ return names;
162
+ }
163
+ const found = new Set();
164
+ const collect = (attrs) => {
165
+ for (const [name, attr] of Object.entries(attrs ?? {})) {
166
+ if (hasBooleanType(attr?.type)) {
167
+ found.add(asciiLowerCase(name));
168
+ }
169
+ }
170
+ };
171
+ for (const attrs of Object.values(spec.def?.['#globalAttrs'] ?? {})) {
172
+ collect(attrs);
173
+ }
174
+ for (const elSpec of spec.specs ?? []) {
175
+ collect(elSpec.attributes);
176
+ }
177
+ names = found;
178
+ booleanAttrNamesCache.set(spec, names);
179
+ return names;
180
+ }
181
+ function hasBooleanType(type) {
182
+ if (type === 'Boolean') {
183
+ return true;
184
+ }
185
+ if (Array.isArray(type)) {
186
+ return type.some(hasBooleanType);
187
+ }
188
+ // A conditional type: `{ condition, type }`
189
+ if (type != null && typeof type === 'object' && 'condition' in type && 'type' in type) {
190
+ return hasBooleanType(type.type);
191
+ }
192
+ return false;
193
+ }
@@ -0,0 +1,16 @@
1
+ import type { MLASTAttr } from '@markuplint/ml-ast';
2
+ /**
3
+ * Marks an AST attribute as made by a pretender. The AST contract of `@markuplint/ml-ast` is that of
4
+ * what a parser reads, so a fact of the core is kept here and not in a field of the node.
5
+ *
6
+ * @param attr - The AST attribute that `MLElement` makes from an attribute of a pretender
7
+ * @returns The same attribute
8
+ */
9
+ export declare function markPretendedAttr<T extends MLASTAttr>(attr: T): T;
10
+ /**
11
+ * Whether an AST attribute was made by a pretender ({@link markPretendedAttr}).
12
+ *
13
+ * @param attr - An AST attribute
14
+ * @returns `true` if the attribute was made by a pretender
15
+ */
16
+ export declare function isPretendedAttr(attr: MLASTAttr): boolean;
@@ -0,0 +1,21 @@
1
+ const pretendedAttrs = new WeakSet();
2
+ /**
3
+ * Marks an AST attribute as made by a pretender. The AST contract of `@markuplint/ml-ast` is that of
4
+ * what a parser reads, so a fact of the core is kept here and not in a field of the node.
5
+ *
6
+ * @param attr - The AST attribute that `MLElement` makes from an attribute of a pretender
7
+ * @returns The same attribute
8
+ */
9
+ export function markPretendedAttr(attr) {
10
+ pretendedAttrs.add(attr);
11
+ return attr;
12
+ }
13
+ /**
14
+ * Whether an AST attribute was made by a pretender ({@link markPretendedAttr}).
15
+ *
16
+ * @param attr - An AST attribute
17
+ * @returns `true` if the attribute was made by a pretender
18
+ */
19
+ export function isPretendedAttr(attr) {
20
+ return pretendedAttrs.has(attr);
21
+ }
@@ -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>[];