@markuplint/ml-spec 5.5.0 → 5.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/lib/algorithm/aria/get-computed-role.js +2 -2
- package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +1 -1
- package/lib/algorithm/aria/get-non-presentational-ancestor.js +4 -4
- package/lib/algorithm/aria/is-exposed.js +7 -2
- package/lib/algorithm/aria/matches-context-role.d.ts +2 -1
- package/lib/algorithm/aria/matches-context-role.js +6 -5
- package/lib/algorithm/dom/tree-boundary.d.ts +64 -49
- package/lib/algorithm/dom/tree-boundary.js +73 -4
- package/lib/types/index.d.ts +50 -3
- package/lib/types/permitted-structures.d.ts +7 -0
- package/lib/utils/directive-resolver.d.ts +11 -0
- package/lib/utils/directive-resolver.js +31 -0
- package/package.json +6 -9
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [5.6.1](https://github.com/markuplint/markuplint/compare/v5.6.0...v5.6.1) (2026-11-13)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **ml-spec:** let a directive pattern declare the key of its repeat ([df95b4c](https://github.com/markuplint/markuplint/commit/df95b4cd56753fa4c665991f6bc091e4c0620225))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# [5.6.0](https://github.com/markuplint/markuplint/compare/v5.5.0...v5.6.0) (2026-11-12)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* do not report an area element that has no parent node in the file ([916a166](https://github.com/markuplint/markuplint/commit/916a166916e4efd5292402abcd293927605ff799)), closes [#4528](https://github.com/markuplint/markuplint/issues/4528)
|
|
18
|
+
* **ml-spec:** follow a light child to the slot that takes it ([5269f1f](https://github.com/markuplint/markuplint/commit/5269f1f3892df6e383a9f08de6ccb07f71b5cfb9))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# [5.5.0](https://github.com/markuplint/markuplint/compare/v5.4.0...v5.5.0) (2026-11-10)
|
|
7
22
|
|
|
8
23
|
### Bug Fixes
|
|
@@ -3,7 +3,7 @@ import { isPresentational } from './is-presentational.js';
|
|
|
3
3
|
import { SVG_NAMESPACE } from '../../const/index.js';
|
|
4
4
|
import { hasSvgAccessibleNameSource } from './accname/svg-helpers.js';
|
|
5
5
|
import { getAttrSpecs } from '../../utils/get-attr-specs.js';
|
|
6
|
-
import { getComposedParentElement } from '../dom/tree-boundary.js';
|
|
6
|
+
import { getAccessibilityParentElement, getComposedParentElement } from '../dom/tree-boundary.js';
|
|
7
7
|
import { getExplicitRole } from './get-explicit-role.js';
|
|
8
8
|
import { getImplicitRole } from './get-implicit-role.js';
|
|
9
9
|
import { getNonPresentationalAncestor } from './get-non-presentational-ancestor.js';
|
|
@@ -325,7 +325,7 @@ el, specs, version) {
|
|
|
325
325
|
if (!computedRole.role?.isImplicit) {
|
|
326
326
|
return false;
|
|
327
327
|
}
|
|
328
|
-
const parent =
|
|
328
|
+
const parent = getAccessibilityParentElement(el);
|
|
329
329
|
if (!parent) {
|
|
330
330
|
return false;
|
|
331
331
|
}
|
|
@@ -3,7 +3,7 @@ import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
|
|
|
3
3
|
* In ARIA 1.3, `generic` role elements are additionally transparent for
|
|
4
4
|
* ownership traversal.
|
|
5
5
|
*
|
|
6
|
-
* The ancestors are those of the accessibility tree: see {@link
|
|
6
|
+
* The ancestors are those of the accessibility tree: see {@link getAccessibilityParentElement}.
|
|
7
7
|
*/
|
|
8
8
|
export declare function getNonPresentationalAncestor(el: Element, specs: MLMLSpec, version: ARIAVersion): import("../../types/index.js").ComputedRole | {
|
|
9
9
|
el: null;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getAccessibilityParentElement } from '../dom/tree-boundary.js';
|
|
2
2
|
import { isTransparentForOwnership } from './is-presentational.js';
|
|
3
3
|
import { getComputedRole } from './get-computed-role.js';
|
|
4
4
|
/**
|
|
5
5
|
* In ARIA 1.3, `generic` role elements are additionally transparent for
|
|
6
6
|
* ownership traversal.
|
|
7
7
|
*
|
|
8
|
-
* The ancestors are those of the accessibility tree: see {@link
|
|
8
|
+
* The ancestors are those of the accessibility tree: see {@link getAccessibilityParentElement}.
|
|
9
9
|
*/
|
|
10
10
|
export function getNonPresentationalAncestor(
|
|
11
11
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
12
12
|
el, specs, version) {
|
|
13
|
-
let ancestor =
|
|
13
|
+
let ancestor = getAccessibilityParentElement(el);
|
|
14
14
|
/**
|
|
15
15
|
* In ARIA 1.1 and 1.2, the role of an element is not affected by the role of its ancestor elements.
|
|
16
16
|
*
|
|
@@ -22,7 +22,7 @@ el, specs, version) {
|
|
|
22
22
|
if (!isTransparentForOwnership(ancestorRole.role?.name, version)) {
|
|
23
23
|
return ancestorRole;
|
|
24
24
|
}
|
|
25
|
-
ancestor =
|
|
25
|
+
ancestor = getAccessibilityParentElement(ancestor);
|
|
26
26
|
}
|
|
27
27
|
return {
|
|
28
28
|
el: ancestor,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ariaSpecs } from './aria-specs.js';
|
|
2
|
-
import { getComposedParentElement } from '../dom/tree-boundary.js';
|
|
2
|
+
import { getComposedParentElement, isNotRendered } from '../dom/tree-boundary.js';
|
|
3
3
|
import { getSpecByTagName } from '../../utils/get-spec-by-tag-name.js';
|
|
4
4
|
import { isPresentational } from './is-presentational.js';
|
|
5
5
|
import { resolveNamespace } from '../../utils/resolve-namespace.js';
|
|
@@ -66,11 +66,16 @@ el, specs, version) {
|
|
|
66
66
|
* that have host language semantics specifying
|
|
67
67
|
* that the element is not displayed, such as CSS display:none,
|
|
68
68
|
* visibility:hidden, or the HTML hidden attribute.
|
|
69
|
+
*
|
|
70
|
+
* An element that is not in the flat tree is not displayed either: a light child of a shadow host
|
|
71
|
+
* that no slot takes.
|
|
69
72
|
*/
|
|
70
73
|
{
|
|
71
74
|
let currentEl = el;
|
|
72
75
|
while (currentEl) {
|
|
73
|
-
if (
|
|
76
|
+
if (isNotRendered(currentEl) ||
|
|
77
|
+
hasDisplayNodeOrVisibilityHidden(currentEl) ||
|
|
78
|
+
currentEl.hasAttribute('hidden')) {
|
|
74
79
|
return true;
|
|
75
80
|
}
|
|
76
81
|
currentEl = getComposedParentElement(currentEl);
|
|
@@ -11,6 +11,7 @@ import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
|
|
|
11
11
|
* `aria-owns` attributes, which is a separate architectural concern.
|
|
12
12
|
* See also: `has-required-owned-elements.ts` has a similar limitation.
|
|
13
13
|
*
|
|
14
|
-
* The chain is the one the accessibility tree is built from ({@link
|
|
14
|
+
* The chain is the one the accessibility tree is built from ({@link getAccessibilityParentElement}), which
|
|
15
|
+
* does not have a `slot`.
|
|
15
16
|
*/
|
|
16
17
|
export declare function matchesContextRole(conditions: readonly string[], ownedEl: Element, specs: MLMLSpec, version: ARIAVersion): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getAccessibilityParentElement } from '../dom/tree-boundary.js';
|
|
2
2
|
import { isTransparentForOwnership } from './is-presentational.js';
|
|
3
3
|
import { getComputedRole } from './get-computed-role.js';
|
|
4
4
|
/**
|
|
@@ -13,12 +13,13 @@ import { getComputedRole } from './get-computed-role.js';
|
|
|
13
13
|
* `aria-owns` attributes, which is a separate architectural concern.
|
|
14
14
|
* See also: `has-required-owned-elements.ts` has a similar limitation.
|
|
15
15
|
*
|
|
16
|
-
* The chain is the one the accessibility tree is built from ({@link
|
|
16
|
+
* The chain is the one the accessibility tree is built from ({@link getAccessibilityParentElement}), which
|
|
17
|
+
* does not have a `slot`.
|
|
17
18
|
*/
|
|
18
19
|
export function matchesContextRole(conditions,
|
|
19
20
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
20
21
|
ownedEl, specs, version) {
|
|
21
|
-
return conditions.some(condition => matchesCondition(condition,
|
|
22
|
+
return conditions.some(condition => matchesCondition(condition, getAccessibilityParentElement(ownedEl), specs, version));
|
|
22
23
|
}
|
|
23
24
|
function matchesCondition(condition,
|
|
24
25
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -50,14 +51,14 @@ parentEl, specs, version) {
|
|
|
50
51
|
* @see https://w3c.github.io/aria/#scope
|
|
51
52
|
*/
|
|
52
53
|
if (isTransparentForOwnership(parentRole?.name, version)) {
|
|
53
|
-
parentEl =
|
|
54
|
+
parentEl = getAccessibilityParentElement(parentEl);
|
|
54
55
|
continue;
|
|
55
56
|
}
|
|
56
57
|
const condition = conditions.shift();
|
|
57
58
|
if (condition !== parentRole?.name) {
|
|
58
59
|
return false;
|
|
59
60
|
}
|
|
60
|
-
parentEl =
|
|
61
|
+
parentEl = getAccessibilityParentElement(parentEl);
|
|
61
62
|
}
|
|
62
63
|
return true;
|
|
63
64
|
}
|
|
@@ -1,47 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module
|
|
3
|
-
*
|
|
4
|
-
* The parent of an element as the DOM sees it, for the algorithms that walk up from an element.
|
|
5
|
-
*
|
|
6
|
-
* The algorithms of this package are written against the DOM `Element`, where `parentElement`
|
|
7
|
-
* stops at the boundary of a tree: the parent element of a node whose parent is not an element
|
|
8
|
-
* (a document fragment, such as the content of a `template` element or a shadow root) is `null`.
|
|
9
|
-
* `@markuplint/ml-core` hands them its own elements, whose `parentElement` is the syntactical
|
|
10
|
-
* parent: the syntax tree has no other place for the content of a `template` element than its
|
|
11
|
-
* children, so the top of the content has the `template` element as its parent element. A walk
|
|
12
|
-
* that reads `parentElement` there leaves the content and enters the tree that holds the
|
|
13
|
-
* `template`. `getRootNode()` is where `@markuplint/ml-core` puts the boundary, and in a real DOM
|
|
14
|
-
* a parent is always in the tree of its child, so comparing the two roots gives the DOM's answer
|
|
15
|
-
* to both.
|
|
16
|
-
*
|
|
17
|
-
* This package cannot use `MLNode#treeParentElement` of `@markuplint/ml-core`, which depends on it,
|
|
18
|
-
* so the helpers derive the same answer from `getRootNode()`. The two are pinned to agree by
|
|
19
|
-
* `tree-scope.spec.ts` of `@markuplint/ml-core`: a change to the boundary in one belongs to both.
|
|
20
|
-
*
|
|
21
|
-
* Which of the two helpers a walk takes depends on what it follows:
|
|
22
|
-
*
|
|
23
|
-
* - {@link getTreeParentElement} follows a relation that the DOM defines in the tree, such as
|
|
24
|
-
* the ancestor `label` element of a labelable element (`findAncestorLabel`, the one walk of
|
|
25
|
-
* this package that does not follow the accessibility tree).
|
|
26
|
-
* - {@link getComposedParentElement} follows what the accessibility tree is built from. Its
|
|
27
|
-
* content is the flat tree: the content of a shadow root is rendered in its host, so the
|
|
28
|
-
* elements above it are the host and the ancestors of the host. The content of an ordinary
|
|
29
|
-
* `template` element is not rendered, and where it is inserted is not known from the markup,
|
|
30
|
-
* so a walk ends at its top.
|
|
31
|
-
*
|
|
32
|
-
* A `template` element that may have a declarative shadow root but whose attachment is not known
|
|
33
|
-
* from the markup (a host that is not in the file, a component, or a dynamic `shadowrootmode`),
|
|
34
|
-
* or that fails to attach one, is treated as an ordinary `template` element: a walk ends at the
|
|
35
|
-
* top of its content.
|
|
36
|
-
*
|
|
37
|
-
* Known limitation: the flat tree places the content of a shadow root where the `slot` element
|
|
38
|
-
* is, between the host and the nodes the host has as children. A walk goes from the top of the
|
|
39
|
-
* shadow root to the host and does not model the position of the `slot`.
|
|
40
|
-
*
|
|
41
|
-
* @see https://dom.spec.whatwg.org/#parent-element
|
|
42
|
-
* @see https://dom.spec.whatwg.org/#concept-shadow-including-ancestor
|
|
43
|
-
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
44
|
-
*/
|
|
45
1
|
/**
|
|
46
2
|
* What the helpers read of an element: the DOM `Element`, and `MLElement` of `@markuplint/ml-core`.
|
|
47
3
|
* An element without `getRootNode` (an adapter that has no notion of a tree boundary) is in one
|
|
@@ -51,6 +7,18 @@ type TreeElement = {
|
|
|
51
7
|
readonly parentElement: unknown;
|
|
52
8
|
getRootNode?(): unknown;
|
|
53
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* What {@link getComposedParentElement} reads besides {@link TreeElement}: the slot that places the
|
|
12
|
+
* element in the flat tree. `null` is an element of a shadow host that no slot takes (not rendered),
|
|
13
|
+
* `undefined` an element that is not one or whose assignment is not known.
|
|
14
|
+
*
|
|
15
|
+
* @template P - The type of the parent element the element reads
|
|
16
|
+
*/
|
|
17
|
+
type FlatTreeElement<P extends TreeElement> = {
|
|
18
|
+
readonly parentElement: P | null;
|
|
19
|
+
getRootNode?(): unknown;
|
|
20
|
+
getFlatTreeSlot?(): P | null | undefined;
|
|
21
|
+
};
|
|
54
22
|
/**
|
|
55
23
|
* The parent element of an element in its own tree, which is what the DOM calls `parentElement`:
|
|
56
24
|
* `null` at the top of the content of a `template` element or of a shadow root, even where the
|
|
@@ -68,16 +36,13 @@ export declare function getTreeParentElement<P extends TreeElement>(el: {
|
|
|
68
36
|
* The parent element of an element as the accessibility tree is built: the parent element in its own
|
|
69
37
|
* tree, or, at the top of a shadow root, the host of the shadow root. `null` at the top of the content
|
|
70
38
|
* of an ordinary `template` element, and at the top of a document or of any other root that is not a
|
|
71
|
-
* shadow root.
|
|
39
|
+
* shadow root. For a light child of a shadow host, the slot that takes it, or `null` when none does.
|
|
72
40
|
*
|
|
73
41
|
* @template P - The type of the parent element the element reads
|
|
74
42
|
* @param el - The element to start from
|
|
75
43
|
* @returns The parent element or the shadow host, or `null` when the walk ends
|
|
76
44
|
*/
|
|
77
|
-
export declare function getComposedParentElement<P extends TreeElement>(el:
|
|
78
|
-
readonly parentElement: P | null;
|
|
79
|
-
getRootNode?(): unknown;
|
|
80
|
-
}): P | null;
|
|
45
|
+
export declare function getComposedParentElement<P extends TreeElement>(el: FlatTreeElement<P>): P | null;
|
|
81
46
|
/**
|
|
82
47
|
* Whether a root is a shadow root: a document fragment that has a host.
|
|
83
48
|
* The shadow root of `@markuplint/ml-core` is not a `ShadowRoot` of the DOM by class, and the
|
|
@@ -90,4 +55,54 @@ export declare function getComposedParentElement<P extends TreeElement>(el: {
|
|
|
90
55
|
export declare function isShadowRoot<P = unknown>(root: unknown): root is {
|
|
91
56
|
readonly host: P;
|
|
92
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* Whether an element is a `slot` element of the HTML namespace, which the accessibility tree does not
|
|
60
|
+
* map: it has no role of its own, and what it renders (the nodes assigned to it, or its children)
|
|
61
|
+
* belongs to the parent of the `slot`. A walk up the ancestors of the accessibility tree that reads
|
|
62
|
+
* roles passes through it. The walks that read an attribute of the ancestors (`hidden`, `aria-hidden`)
|
|
63
|
+
* do not skip it: what a `slot` that has them renders is hidden as well.
|
|
64
|
+
*
|
|
65
|
+
* It does not look at `MLParser#slotElement` on purpose: the `slot` of a framework (Vue, Svelte, Astro)
|
|
66
|
+
* is replaced with what it is given, so it is no element of the accessibility tree either, and the
|
|
67
|
+
* pass-through is right for it too. Which `slot` has an assignment is `MLElement#getAssignedNodes` of
|
|
68
|
+
* `@markuplint/ml-core`, which does look at it.
|
|
69
|
+
*
|
|
70
|
+
* > No corresponding role
|
|
71
|
+
*
|
|
72
|
+
* @param el - An element
|
|
73
|
+
* @returns `true` for the `slot` element of the HTML namespace
|
|
74
|
+
* @see https://w3c.github.io/html-aam/#el-slot
|
|
75
|
+
* @see https://dom.spec.whatwg.org/#find-flattened-slotables
|
|
76
|
+
*/
|
|
77
|
+
export declare function isSlotElement(el: {
|
|
78
|
+
readonly localName: string;
|
|
79
|
+
readonly namespaceURI?: string | null;
|
|
80
|
+
}): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Whether an element is not rendered: it is a light child of a shadow host, and no slot of the shadow
|
|
83
|
+
* root takes it (`getFlatTreeSlot()` is `null`). The flat tree has no place for it, so it is not in the
|
|
84
|
+
* accessibility tree either, whatever its host is.
|
|
85
|
+
*
|
|
86
|
+
* @param el - An element
|
|
87
|
+
* @returns `true` for an element that is known not to be rendered
|
|
88
|
+
* @see https://dom.spec.whatwg.org/#find-flattened-slotables
|
|
89
|
+
*/
|
|
90
|
+
export declare function isNotRendered(el: object): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* The parent element of an element in the accessibility tree: the composed parent element
|
|
93
|
+
* ({@link getComposedParentElement}) with the `slot` elements passed through ({@link isSlotElement}). The walks
|
|
94
|
+
* that read the roles of the ancestors take it; the walks that read an attribute of the ancestors take
|
|
95
|
+
* {@link getComposedParentElement}, as what a `slot` that has `hidden` renders is hidden.
|
|
96
|
+
*
|
|
97
|
+
* @template E - The type of the element
|
|
98
|
+
* @param el - The element to start from
|
|
99
|
+
* @returns The parent element, or `null` when the walk ends
|
|
100
|
+
*/
|
|
101
|
+
export declare function getAccessibilityParentElement<E extends {
|
|
102
|
+
readonly parentElement: E | null;
|
|
103
|
+
readonly localName: string;
|
|
104
|
+
readonly namespaceURI?: string | null;
|
|
105
|
+
getRootNode?(): unknown;
|
|
106
|
+
getFlatTreeSlot?(): E | null | undefined;
|
|
107
|
+
}>(el: E): E | null;
|
|
93
108
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { XHTML_NAMESPACE } from '../../const/dom.js';
|
|
1
2
|
/**
|
|
2
3
|
* @module
|
|
3
4
|
*
|
|
@@ -34,10 +35,22 @@
|
|
|
34
35
|
* or that fails to attach one, is treated as an ordinary `template` element: a walk ends at the
|
|
35
36
|
* top of its content.
|
|
36
37
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* shadow root
|
|
38
|
+
* The flat tree renders a light child of a shadow host where the `slot` element that takes it is,
|
|
39
|
+
* so the parent of the child is that `slot`: `getFlatTreeSlot()` of `@markuplint/ml-core` tells it
|
|
40
|
+
* (the named slot assignment of the shadow root). A child that no slot takes is not rendered, so a
|
|
41
|
+
* walk ends there as it does at the top of the content of an ordinary `template` element. Where the
|
|
42
|
+
* assignment is not known (`undefined`: manual assignment, a name written as code, a slot of a
|
|
43
|
+
* framework), the walk goes to the host as it does for any other child. The `slot` element itself
|
|
44
|
+
* is an element that is not mapped to the accessibility tree, which the algorithms of this package
|
|
45
|
+
* pass through (`isSlotElement`).
|
|
40
46
|
*
|
|
47
|
+
* Known limitation: a walk from the top of the shadow root goes to the host and does not model where
|
|
48
|
+
* the `slot` sits in the shadow root, so the elements of the shadow root that are above the `slot` are
|
|
49
|
+
* not between the host and a light child the `slot` takes. The fallback content of a slot that takes
|
|
50
|
+
* nodes is not rendered either, and a walk from it still goes through the `slot`.
|
|
51
|
+
*
|
|
52
|
+
* @see https://dom.spec.whatwg.org/#find-a-slot
|
|
53
|
+
* @see https://dom.spec.whatwg.org/#find-flattened-slotables
|
|
41
54
|
* @see https://dom.spec.whatwg.org/#parent-element
|
|
42
55
|
* @see https://dom.spec.whatwg.org/#concept-shadow-including-ancestor
|
|
43
56
|
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
@@ -68,13 +81,17 @@ export function getTreeParentElement(el) {
|
|
|
68
81
|
* The parent element of an element as the accessibility tree is built: the parent element in its own
|
|
69
82
|
* tree, or, at the top of a shadow root, the host of the shadow root. `null` at the top of the content
|
|
70
83
|
* of an ordinary `template` element, and at the top of a document or of any other root that is not a
|
|
71
|
-
* shadow root.
|
|
84
|
+
* shadow root. For a light child of a shadow host, the slot that takes it, or `null` when none does.
|
|
72
85
|
*
|
|
73
86
|
* @template P - The type of the parent element the element reads
|
|
74
87
|
* @param el - The element to start from
|
|
75
88
|
* @returns The parent element or the shadow host, or `null` when the walk ends
|
|
76
89
|
*/
|
|
77
90
|
export function getComposedParentElement(el) {
|
|
91
|
+
const slot = el.getFlatTreeSlot?.();
|
|
92
|
+
if (slot !== undefined) {
|
|
93
|
+
return slot;
|
|
94
|
+
}
|
|
78
95
|
const parent = getTreeParentElement(el);
|
|
79
96
|
if (parent) {
|
|
80
97
|
return parent;
|
|
@@ -101,3 +118,55 @@ export function isShadowRoot(root) {
|
|
|
101
118
|
root.nodeType === DOCUMENT_FRAGMENT_NODE &&
|
|
102
119
|
'host' in root);
|
|
103
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Whether an element is a `slot` element of the HTML namespace, which the accessibility tree does not
|
|
123
|
+
* map: it has no role of its own, and what it renders (the nodes assigned to it, or its children)
|
|
124
|
+
* belongs to the parent of the `slot`. A walk up the ancestors of the accessibility tree that reads
|
|
125
|
+
* roles passes through it. The walks that read an attribute of the ancestors (`hidden`, `aria-hidden`)
|
|
126
|
+
* do not skip it: what a `slot` that has them renders is hidden as well.
|
|
127
|
+
*
|
|
128
|
+
* It does not look at `MLParser#slotElement` on purpose: the `slot` of a framework (Vue, Svelte, Astro)
|
|
129
|
+
* is replaced with what it is given, so it is no element of the accessibility tree either, and the
|
|
130
|
+
* pass-through is right for it too. Which `slot` has an assignment is `MLElement#getAssignedNodes` of
|
|
131
|
+
* `@markuplint/ml-core`, which does look at it.
|
|
132
|
+
*
|
|
133
|
+
* > No corresponding role
|
|
134
|
+
*
|
|
135
|
+
* @param el - An element
|
|
136
|
+
* @returns `true` for the `slot` element of the HTML namespace
|
|
137
|
+
* @see https://w3c.github.io/html-aam/#el-slot
|
|
138
|
+
* @see https://dom.spec.whatwg.org/#find-flattened-slotables
|
|
139
|
+
*/
|
|
140
|
+
export function isSlotElement(el) {
|
|
141
|
+
return el.localName === 'slot' && el.namespaceURI === XHTML_NAMESPACE;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Whether an element is not rendered: it is a light child of a shadow host, and no slot of the shadow
|
|
145
|
+
* root takes it (`getFlatTreeSlot()` is `null`). The flat tree has no place for it, so it is not in the
|
|
146
|
+
* accessibility tree either, whatever its host is.
|
|
147
|
+
*
|
|
148
|
+
* @param el - An element
|
|
149
|
+
* @returns `true` for an element that is known not to be rendered
|
|
150
|
+
* @see https://dom.spec.whatwg.org/#find-flattened-slotables
|
|
151
|
+
*/
|
|
152
|
+
export function isNotRendered(el) {
|
|
153
|
+
// The DOM `Element` has no `getFlatTreeSlot`: only the element of `@markuplint/ml-core` knows it
|
|
154
|
+
return el.getFlatTreeSlot?.() === null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The parent element of an element in the accessibility tree: the composed parent element
|
|
158
|
+
* ({@link getComposedParentElement}) with the `slot` elements passed through ({@link isSlotElement}). The walks
|
|
159
|
+
* that read the roles of the ancestors take it; the walks that read an attribute of the ancestors take
|
|
160
|
+
* {@link getComposedParentElement}, as what a `slot` that has `hidden` renders is hidden.
|
|
161
|
+
*
|
|
162
|
+
* @template E - The type of the element
|
|
163
|
+
* @param el - The element to start from
|
|
164
|
+
* @returns The parent element, or `null` when the walk ends
|
|
165
|
+
*/
|
|
166
|
+
export function getAccessibilityParentElement(el) {
|
|
167
|
+
let parent = getComposedParentElement(el);
|
|
168
|
+
while (parent && isSlotElement(parent)) {
|
|
169
|
+
parent = getComposedParentElement(parent);
|
|
170
|
+
}
|
|
171
|
+
return parent;
|
|
172
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -155,11 +155,58 @@ export type DirectivePattern = {
|
|
|
155
155
|
*/
|
|
156
156
|
readonly valueType?: 'string' | 'number' | 'boolean' | 'code';
|
|
157
157
|
/**
|
|
158
|
-
* If true, the attribute
|
|
159
|
-
* Can also be a string array of potentialName values for which
|
|
160
|
-
*
|
|
158
|
+
* If true, the attribute is not a repeat of an attribute of the same name (`:class` beside `class`,
|
|
159
|
+
* which Vue merges). Can also be a string array of potentialName values for which this holds
|
|
160
|
+
* (e.g., ['class', 'style']).
|
|
161
|
+
*
|
|
162
|
+
* It is not whether the attribute may be written twice: that is told by {@link DirectivePattern.duplicateKey}
|
|
163
|
+
* (without it, the same spelling of a name that is not `true` is a repeat, and `true` is never one).
|
|
161
164
|
*/
|
|
162
165
|
readonly isDuplicatable?: true | readonly string[];
|
|
166
|
+
/**
|
|
167
|
+
* The key under which the framework counts this directive as written twice, as a template that takes the
|
|
168
|
+
* captures of `pattern` (`$1`, `$2`, …) as they are spelled. Two attributes of an element with the same key
|
|
169
|
+
* are a repeat, whatever name the directive resolves to (`potentialName`): a framework tells a repeat by
|
|
170
|
+
* what it keeps of the name, and a directive is not an attribute of the DOM.
|
|
171
|
+
*
|
|
172
|
+
* Without it, a pattern is keyed by the name as it is written (how a framework that rejects only the
|
|
173
|
+
* same spelling of a directive tells a repeat), except a pattern with `isDuplicatable: true`, which is
|
|
174
|
+
* never a repeat (a framework that takes any number of the same directive).
|
|
175
|
+
*
|
|
176
|
+
* @example "$1" -- the key of `:view-box.camel` is its argument, `view-box` (with `duplicateKeyModifiers`, `viewBox`)
|
|
177
|
+
*/
|
|
178
|
+
readonly duplicateKey?: string;
|
|
179
|
+
/**
|
|
180
|
+
* Modifiers of the directive that change {@link DirectivePattern.duplicateKey}: the modifiers are those
|
|
181
|
+
* written after the name, and a framework gives two spellings of a name that differ in them the same key
|
|
182
|
+
* (the order of them, a modifier that it does not know) or a different one (a modifier that changes the
|
|
183
|
+
* name that is bound). Only the modifiers listed here count, and a modifier is matched as it is spelled.
|
|
184
|
+
*/
|
|
185
|
+
readonly duplicateKeyModifiers?: {
|
|
186
|
+
/**
|
|
187
|
+
* The capture group of `pattern` that holds the modifiers, joined with `.` and starting with it (`.prop.camel`).
|
|
188
|
+
*/
|
|
189
|
+
readonly group: number;
|
|
190
|
+
/**
|
|
191
|
+
* The modifiers that turn the template into camelCase before the prefixes are put in front of it.
|
|
192
|
+
*/
|
|
193
|
+
readonly camelize?: readonly string[];
|
|
194
|
+
/**
|
|
195
|
+
* The prefix that a modifier puts in front of the key. They are put in the order listed, so that
|
|
196
|
+
* the first of the listed modifiers that is written is the outermost.
|
|
197
|
+
*/
|
|
198
|
+
readonly prefixes?: readonly {
|
|
199
|
+
readonly modifier: string;
|
|
200
|
+
readonly prefix: string;
|
|
201
|
+
}[];
|
|
202
|
+
/**
|
|
203
|
+
* When no prefix is put in front of the key, it is the name of an attribute (`:id.foo` binds `id`), so
|
|
204
|
+
* the element folds its case as it does for any attribute and it is a repeat of that attribute
|
|
205
|
+
* written as such (`:id`). A key with a prefix is a property or a namespaced name, which
|
|
206
|
+
* keeps its case (`.id` and `.ID` are two).
|
|
207
|
+
*/
|
|
208
|
+
readonly foldedWithoutPrefix?: true;
|
|
209
|
+
};
|
|
163
210
|
};
|
|
164
211
|
/**
|
|
165
212
|
* Reference URLs
|
|
@@ -12,7 +12,14 @@ export interface ContentModelsSchema {
|
|
|
12
12
|
}
|
|
13
13
|
export interface ContentModel {
|
|
14
14
|
contents: PermittedContentPattern[] | boolean;
|
|
15
|
+
/**
|
|
16
|
+
* A CSS selector of the ancestor that this element must have. The ancestor is looked for in the tree of the element: the walk up the tree stops at the content of a `template` element.
|
|
17
|
+
*/
|
|
15
18
|
descendantOf?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Whether `descendantOf` is required only of an element that has a parent node, as the HTML Standard says of the `area` element ("An area element with a parent node must have a map element ancestor"). The top of a partial or of a component has none in the file, so the element there is not reported.
|
|
21
|
+
*/
|
|
22
|
+
descendantOfWithParentNode?: boolean;
|
|
16
23
|
/**
|
|
17
24
|
* Elements that this element must NOT appear as a descendant of. Each entry is a CSS selector matching forbidden ancestor elements. The walk up the tree stops at the content of a `template` element, which has no ancestors.
|
|
18
25
|
*/
|
|
@@ -22,6 +22,17 @@ export type DirectiveResolution = {
|
|
|
22
22
|
readonly expressionLanguage?: ExpressionLanguage;
|
|
23
23
|
readonly valueType?: 'string' | 'number' | 'boolean' | 'code';
|
|
24
24
|
readonly isDuplicatable?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Present only when the matched pattern decides it ({@link DirectivePattern.duplicateKey}): the key
|
|
27
|
+
* under which the directive counts as written twice, or `null` when it never does (a pattern that is
|
|
28
|
+
* `isDuplicatable: true` and declares no key). Absent, the key is the name as it is written.
|
|
29
|
+
*/
|
|
30
|
+
readonly duplicateKey?: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Present only when the key is the name of an attribute that the element folds
|
|
33
|
+
* ({@link DirectivePattern.duplicateKeyModifiers}'s `foldedWithoutPrefix`) and no prefix is in front of it.
|
|
34
|
+
*/
|
|
35
|
+
readonly duplicateKeyFolds?: true;
|
|
25
36
|
};
|
|
26
37
|
/**
|
|
27
38
|
* Resolves an attribute name against a list of compiled directive patterns.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { camelize } from '@markuplint/shared';
|
|
1
2
|
const cache = new WeakMap();
|
|
2
3
|
/**
|
|
3
4
|
* Compiles directive patterns into RegExp objects, caching them
|
|
@@ -14,6 +15,33 @@ export function compileDirectivePatterns(patterns) {
|
|
|
14
15
|
}
|
|
15
16
|
return compiled;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The key of an attribute that a pattern gives, as {@link DirectivePattern.duplicateKey} says.
|
|
20
|
+
*/
|
|
21
|
+
function resolveDuplicateKey(
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
23
|
+
pattern, match) {
|
|
24
|
+
if (pattern.duplicateKey == null) {
|
|
25
|
+
return { key: pattern.isDuplicatable === true ? null : undefined, folds: false };
|
|
26
|
+
}
|
|
27
|
+
const modifiers = pattern.duplicateKeyModifiers;
|
|
28
|
+
const written = new Set((modifiers ? (match[modifiers.group] ?? '') : '').split('.').filter(Boolean));
|
|
29
|
+
// Only what the directive writes is camelized (`transformBind` of Vue does so to the argument), not the
|
|
30
|
+
// text that the template puts around it
|
|
31
|
+
const camelized = modifiers?.camelize?.some(modifier => written.has(modifier)) === true;
|
|
32
|
+
const key = pattern.duplicateKey.replaceAll(/\$(\d+)/g, (_, idx) => {
|
|
33
|
+
const capture = match[Number(idx)] ?? '';
|
|
34
|
+
return camelized ? camelize(capture) : capture;
|
|
35
|
+
});
|
|
36
|
+
if (!modifiers) {
|
|
37
|
+
return { key, folds: false };
|
|
38
|
+
}
|
|
39
|
+
const prefix = (modifiers.prefixes ?? [])
|
|
40
|
+
.filter(({ modifier }) => written.has(modifier))
|
|
41
|
+
.map(({ prefix }) => prefix)
|
|
42
|
+
.join('');
|
|
43
|
+
return { key: prefix + key, folds: modifiers.foldedWithoutPrefix === true && prefix === '' };
|
|
44
|
+
}
|
|
17
45
|
/**
|
|
18
46
|
* Resolves an attribute name against a list of compiled directive patterns.
|
|
19
47
|
* Returns the first match's resolution, or null if no pattern matches.
|
|
@@ -38,6 +66,7 @@ compiledPatterns) {
|
|
|
38
66
|
else if (Array.isArray(pattern.isDuplicatable) && potentialName) {
|
|
39
67
|
isDuplicatable = pattern.isDuplicatable.includes(potentialName);
|
|
40
68
|
}
|
|
69
|
+
const duplicateKey = resolveDuplicateKey(pattern, match);
|
|
41
70
|
return {
|
|
42
71
|
potentialName,
|
|
43
72
|
isDirective: pattern.isDirective,
|
|
@@ -45,6 +74,8 @@ compiledPatterns) {
|
|
|
45
74
|
valueType: pattern.valueType,
|
|
46
75
|
isDuplicatable,
|
|
47
76
|
...(pattern.expressionLanguage ? { expressionLanguage: pattern.expressionLanguage } : {}),
|
|
77
|
+
...(duplicateKey.key === undefined ? {} : { duplicateKey: duplicateKey.key }),
|
|
78
|
+
...(duplicateKey.folds ? { duplicateKeyFolds: true } : {}),
|
|
48
79
|
};
|
|
49
80
|
}
|
|
50
81
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/ml-spec",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.1",
|
|
4
4
|
"description": "Types and schema that specs of the Markup languages for markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,9 +22,6 @@
|
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
|
-
"typedoc": {
|
|
26
|
-
"entryPoint": "./src/index.ts"
|
|
27
|
-
},
|
|
28
25
|
"scripts": {
|
|
29
26
|
"build": "tsc --project tsconfig.build.json",
|
|
30
27
|
"dev": "tsc --watch --project tsconfig.build.json",
|
|
@@ -36,15 +33,15 @@
|
|
|
36
33
|
"schema:aria": "npx json2ts ./schemas/aria.schema.json --cwd ./schemas > ./src/types/aria.ts"
|
|
37
34
|
},
|
|
38
35
|
"dependencies": {
|
|
39
|
-
"@markuplint/ml-ast": "5.
|
|
40
|
-
"@markuplint/shared": "5.
|
|
41
|
-
"@markuplint/types": "5.
|
|
36
|
+
"@markuplint/ml-ast": "5.6.0",
|
|
37
|
+
"@markuplint/shared": "5.6.0",
|
|
38
|
+
"@markuplint/types": "5.6.0",
|
|
42
39
|
"is-plain-object": "^5.1.0",
|
|
43
40
|
"type-fest": "^5.10.0"
|
|
44
41
|
},
|
|
45
42
|
"devDependencies": {
|
|
46
|
-
"@markuplint/test-tools": "5.
|
|
43
|
+
"@markuplint/test-tools": "5.6.1",
|
|
47
44
|
"json-schema-to-typescript": "16.0.0"
|
|
48
45
|
},
|
|
49
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "a9245162df3699c4880469f493aaaf6103343d83"
|
|
50
47
|
}
|