@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.
- package/CHANGELOG.md +29 -0
- package/lib/ml-core.js +3 -0
- package/lib/ml-dom/helper/accname.d.ts +35 -0
- package/lib/ml-dom/helper/accname.js +69 -17
- package/lib/ml-dom/helper/debug.js +1 -1
- package/lib/ml-dom/helper/literal-values.d.ts +102 -0
- package/lib/ml-dom/helper/literal-values.js +193 -0
- package/lib/ml-dom/helper/pretended-attrs.d.ts +16 -0
- package/lib/ml-dom/helper/pretended-attrs.js +21 -0
- 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 +130 -0
- package/lib/ml-dom/helper/tree-scope.js +341 -0
- package/lib/ml-dom/helper/walkers.d.ts +15 -0
- package/lib/ml-dom/helper/walkers.js +34 -0
- package/lib/ml-dom/index.d.ts +4 -0
- package/lib/ml-dom/index.js +2 -0
- package/lib/ml-dom/node/attr.d.ts +77 -10
- package/lib/ml-dom/node/attr.js +209 -20
- package/lib/ml-dom/node/block.d.ts +15 -6
- package/lib/ml-dom/node/block.js +17 -8
- package/lib/ml-dom/node/document-fragment.d.ts +18 -6
- package/lib/ml-dom/node/document-fragment.js +19 -7
- package/lib/ml-dom/node/document.d.ts +36 -7
- package/lib/ml-dom/node/document.js +60 -17
- package/lib/ml-dom/node/dom-token-list.js +28 -3
- package/lib/ml-dom/node/element.d.ts +148 -24
- package/lib/ml-dom/node/element.js +258 -48
- package/lib/ml-dom/node/named-node-map.js +2 -1
- package/lib/ml-dom/node/node-list.js +2 -3
- package/lib/ml-dom/node/node.d.ts +55 -10
- package/lib/ml-dom/node/node.js +98 -13
- package/lib/ml-dom/node/parent-node.d.ts +39 -8
- package/lib/ml-dom/node/parent-node.js +79 -12
- package/lib/ml-dom/node/shadow-root.d.ts +162 -0
- package/lib/ml-dom/node/shadow-root.js +219 -0
- package/lib/ml-dom/node/text.d.ts +12 -3
- package/lib/ml-dom/node/text.js +14 -3
- package/lib/ml-rule/ml-rule.js +4 -1
- package/lib/test/index.js +3 -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,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,
|
|
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
|
-
|
|
116
|
-
return found ?? null;
|
|
177
|
+
return root?.getElementById(decodeEntities(id)) ?? null;
|
|
117
178
|
},
|
|
118
179
|
getLabelsForId(id) {
|
|
119
|
-
const
|
|
120
|
-
return
|
|
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
|
-
|
|
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>[];
|