@markuplint/ml-spec 5.4.0 → 5.5.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 (46) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +4 -0
  3. package/lib/algorithm/aria/accname/aria-steps.d.ts +12 -7
  4. package/lib/algorithm/aria/accname/aria-steps.js +16 -7
  5. package/lib/algorithm/aria/accname/compute.d.ts +3 -3
  6. package/lib/algorithm/aria/accname/compute.js +3 -3
  7. package/lib/algorithm/aria/accname/element-names.d.ts +2 -2
  8. package/lib/algorithm/aria/accname/element-names.js +29 -29
  9. package/lib/algorithm/aria/accname/helpers.d.ts +30 -9
  10. package/lib/algorithm/aria/accname/helpers.js +34 -12
  11. package/lib/algorithm/aria/accname/label-steps.d.ts +2 -2
  12. package/lib/algorithm/aria/accname/label-steps.js +2 -2
  13. package/lib/algorithm/aria/accname/svg-helpers.d.ts +2 -2
  14. package/lib/algorithm/aria/accname/svg-helpers.js +2 -2
  15. package/lib/algorithm/aria/accname/types.d.ts +12 -0
  16. package/lib/algorithm/aria/accname-computation.d.ts +1 -1
  17. package/lib/algorithm/aria/accname-computation.js +6 -6
  18. package/lib/algorithm/aria/get-aria.js +2 -2
  19. package/lib/algorithm/aria/get-computed-role.js +14 -9
  20. package/lib/algorithm/aria/get-explicit-role.d.ts +2 -2
  21. package/lib/algorithm/aria/get-explicit-role.js +11 -4
  22. package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +2 -0
  23. package/lib/algorithm/aria/get-non-presentational-ancestor.js +5 -2
  24. package/lib/algorithm/aria/is-exposed.d.ts +3 -1
  25. package/lib/algorithm/aria/is-exposed.js +15 -7
  26. package/lib/algorithm/aria/is-presentational.d.ts +2 -3
  27. package/lib/algorithm/aria/is-presentational.js +2 -3
  28. package/lib/algorithm/aria/matches-context-role.d.ts +3 -1
  29. package/lib/algorithm/aria/matches-context-role.js +10 -7
  30. package/lib/algorithm/dom/tree-boundary.d.ts +93 -0
  31. package/lib/algorithm/dom/tree-boundary.js +103 -0
  32. package/lib/algorithm/html/content-model-category-to-tag-names.d.ts +7 -3
  33. package/lib/algorithm/html/content-model-category-to-tag-names.js +7 -3
  34. package/lib/algorithm/html/select-display-size.d.ts +29 -0
  35. package/lib/algorithm/html/select-display-size.js +38 -0
  36. package/lib/const/accname.d.ts +1 -1
  37. package/lib/const/accname.js +1 -1
  38. package/lib/index.d.ts +2 -0
  39. package/lib/index.js +3 -0
  40. package/lib/types/index.d.ts +63 -2
  41. package/lib/types/permitted-structures.d.ts +5 -1
  42. package/lib/utils/directive-resolver.d.ts +8 -0
  43. package/lib/utils/directive-resolver.js +1 -0
  44. package/lib/utils/schema-to-spec.d.ts +4 -0
  45. package/lib/utils/schema-to-spec.js +42 -0
  46. package/package.json +6 -5
@@ -31,7 +31,7 @@ const computingElements = new WeakSet();
31
31
  * @param specs - The ML specification data for role resolution
32
32
  * @param version - The ARIA version to use for role computation
33
33
  * @returns The computed accessible name string, or an empty string if none is found
34
- * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
34
+ * @see https://w3c.github.io/accname/#computation-steps — AccName 1.2 §4.3.2
35
35
  */
36
36
  export function getAccname(
37
37
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -130,15 +130,15 @@ el, specs, version) {
130
130
  };
131
131
  }
132
132
  /**
133
- * Walks up the DOM tree to find the root node (Document or DocumentFragment).
133
+ * Finds the root node (Document or DocumentFragment) of the tree the element is in: for an
134
+ * element in the content of a `template` element or a shadow root, that content, not the
135
+ * document. A host that holds the content in the syntax tree under the `template` element (the
136
+ * markuplint DOM) answers it in `getRootNode()`, as the DOM does.
134
137
  */
135
138
  function getRootNode(
136
139
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
137
140
  el) {
138
- let root = el;
139
- while (root.parentNode) {
140
- root = root.parentNode;
141
- }
141
+ const root = el.getRootNode();
142
142
  if (root.nodeType === 9 || root.nodeType === 11) {
143
143
  return root;
144
144
  }
@@ -89,14 +89,14 @@ function optimizePermittedRoles(permittedRoles, version) {
89
89
  return permittedRoles;
90
90
  }
91
91
  const unique = new Set(permittedRoles);
92
- // https://www.w3.org/TR/wai-aria-1.2/#note-regarding-the-aria-1-1-none-role
92
+ // https://w3c.github.io/aria/#none
93
93
  if (unique.has('presentation')) {
94
94
  unique.add('none');
95
95
  }
96
96
  if (unique.has('none')) {
97
97
  unique.add('presentation');
98
98
  }
99
- // https://w3c.github.io/aria/#ref-for-image
99
+ // https://w3c.github.io/aria/#image
100
100
  // In ARIA 1.3, `image` is the primary role name and `img` is a synonym.
101
101
  if (version === '1.3') {
102
102
  if (unique.has('image')) {
@@ -3,6 +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
7
  import { getExplicitRole } from './get-explicit-role.js';
7
8
  import { getImplicitRole } from './get-implicit-role.js';
8
9
  import { getNonPresentationalAncestor } from './get-non-presentational-ancestor.js';
@@ -98,6 +99,7 @@ el, version, assumeSingleNode) {
98
99
  * "accessibility child" and "accessibility parent", and
99
100
  * `matchesContextRole` additionally skips `generic` elements.
100
101
  *
102
+ * @see https://w3c.github.io/aria/#conflict_resolution_presentation_none
101
103
  * @see https://w3c.github.io/aria/#scope
102
104
  * @see https://github.com/w3c/aria/issues/1033
103
105
  * @see https://github.com/w3c/aria/pull/1454
@@ -115,12 +117,14 @@ el, version, assumeSingleNode) {
115
117
  !isNativeContextIntact(computedRole, el, specs, version)) {
116
118
  /**
117
119
  * An element fragment that serves as the root without a parent element
118
- * cannot satisfy the "Required Accessibility Parent Role" condition.
120
+ * (or the top of the content of an ordinary `template` element, whose
121
+ * placement is not known) cannot satisfy the "Required Accessibility
122
+ * Parent Role" condition.
119
123
  * Therefore, under normal circumstances, the `role` will disappear.
120
124
  * However, in this specific case, it will fall back to both explicit
121
125
  * and implicit roles. Note that the explicit role takes precedence.
122
126
  */
123
- if (el.parentElement === null) {
127
+ if (getComposedParentElement(el) === null) {
124
128
  return {
125
129
  ...computedRole,
126
130
  errorType: 'NO_OWNER',
@@ -145,8 +149,7 @@ el, version, assumeSingleNode) {
145
149
  /**
146
150
  * SVG: Including Elements in the Accessibility Tree
147
151
  *
148
- * > Many SVG elements—although rendered to the screen—
149
- * > do not have an intrinsic semantic meaning. Instead,
152
+ * > Many SVG elements—although rendered to the screen—do not have an intrinsic semantic meaning. Instead,
150
153
  * > they represent components of the visual presentation of the document.
151
154
  * > To simplify the accessible representation of the document,
152
155
  * > these purely presentational elements should normally be omitted
@@ -166,11 +169,10 @@ el, version, assumeSingleNode) {
166
169
  * > - the use element
167
170
  * > - the grouping (g) element
168
171
  * > - the image element
169
- * > - the mesh element
170
172
  * > - text formatting elements (textPath, tspan)
171
173
  * > - the foreignObject element
172
174
  *
173
- * @see https://www.w3.org/TR/svg-aam-1.0/#include_elements
175
+ * @see https://www.w3.org/TR/svg-aam/#include_elements
174
176
  */
175
177
  if (
176
178
  // It doesn't been specified a valid explicit role.
@@ -189,10 +191,12 @@ el, version, assumeSingleNode) {
189
191
  const implicitRole = lazyImplicitRole ?? getImplicitRole(specs, el, version);
190
192
  /**
191
193
  * > If an element is focusable,
192
- * > user agents MUST ignore the presentation/none role
194
+ * > user agents MUST ignore the none/presentation role
193
195
  * > and expose the element with its implicit role,
194
196
  * > in order to ensure that the element is operable.
195
197
  *
198
+ * @see https://w3c.github.io/aria/#conflict_resolution_presentation_none
199
+ *
196
200
  * With the issue: What does focusable or otherwise interactive mean](https://github.com/w3c/aria/issues/1192)
197
201
  *
198
202
  * - Focus: https://html.spec.whatwg.org/multipage/interaction.html#focus
@@ -286,6 +290,7 @@ el, specs, attrName) {
286
290
  }
287
291
  /**
288
292
  * Determines whether some ancestors match the condition that the specified callback function.
293
+ * The ancestors are those of the accessibility tree: see {@link getComposedParentElement}.
289
294
  */
290
295
  function someAncestors(
291
296
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -294,7 +299,7 @@ el, predicate) {
294
299
  let current = el;
295
300
  while (current) {
296
301
  list.push(current);
297
- current = current.parentElement;
302
+ current = getComposedParentElement(current);
298
303
  }
299
304
  return list.some(predicate);
300
305
  }
@@ -320,7 +325,7 @@ el, specs, version) {
320
325
  if (!computedRole.role?.isImplicit) {
321
326
  return false;
322
327
  }
323
- const parent = el.parentElement;
328
+ const parent = getComposedParentElement(el);
324
329
  if (!parent) {
325
330
  return false;
326
331
  }
@@ -4,10 +4,10 @@ import type { ARIAVersion, ComputedRole, MLMLSpec } from '../../types/index.js';
4
4
  *
5
5
  * The roles that ARIA in HTML does not permit on the element are authoring
6
6
  * errors that the user agent does not correct: such a role is returned with
7
- * `errorType: 'NO_PERMITTED'` (Core-AAM §3.2), except on an element that is
7
+ * `errorType: 'NO_PERMITTED'` (Core-AAM, the conflicts between the mappings), except on an element that is
8
8
  * never rendered.
9
9
  *
10
10
  * @see https://w3c.github.io/aria/#document-handling_author-errors
11
- * @see https://w3c.github.io/core-aam/#mapping_conflicts
11
+ * @see https://w3c.github.io/core-aam/#mapping_conflicts — Core-AAM §3.2
12
12
  */
13
13
  export declare function getExplicitRole(specs: MLMLSpec, el: Element, version: ARIAVersion): ComputedRole;
@@ -1,3 +1,4 @@
1
+ import { decodeEntities } from '@markuplint/shared';
1
2
  import { XHTML_NAMESPACE } from '../../const/index.js';
2
3
  import { getRoleSpec } from './get-role-spec.js';
3
4
  import { resolveNamespace } from '../../utils/resolve-namespace.js';
@@ -7,11 +8,11 @@ import { getPermittedRoles } from './get-permitted-roles.js';
7
8
  *
8
9
  * The roles that ARIA in HTML does not permit on the element are authoring
9
10
  * errors that the user agent does not correct: such a role is returned with
10
- * `errorType: 'NO_PERMITTED'` (Core-AAM §3.2), except on an element that is
11
+ * `errorType: 'NO_PERMITTED'` (Core-AAM, the conflicts between the mappings), except on an element that is
11
12
  * never rendered.
12
13
  *
13
14
  * @see https://w3c.github.io/aria/#document-handling_author-errors
14
- * @see https://w3c.github.io/core-aam/#mapping_conflicts
15
+ * @see https://w3c.github.io/core-aam/#mapping_conflicts — Core-AAM §3.2
15
16
  */
16
17
  export function getExplicitRole(specs,
17
18
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -162,8 +163,14 @@ el, role) {
162
163
  if (el.getAttribute('aria-label')) {
163
164
  return true;
164
165
  }
165
- const id = el.getAttribute('aria-labelledby');
166
- if (id && el.ownerDocument.getElementById(id)) {
166
+ // `aria-labelledby` is a list of ids, and each is resolved in the tree of the element: the
167
+ // content of a `template` element and a shadow root are trees of their own. A DOM has resolved the
168
+ // character references of a token already, but the markuplint DOM gives it as written in the markup,
169
+ // so the token is looked for as it is, and then with its character references resolved.
170
+ const ids = el.getAttribute('aria-labelledby')?.split(/\s+/).filter(Boolean) ?? [];
171
+ const root = el.getRootNode();
172
+ if ((root.nodeType === 9 || root.nodeType === 11) &&
173
+ ids.some(id => root.getElementById(id) ?? root.getElementById(decodeEntities(id)))) {
167
174
  return true;
168
175
  }
169
176
  }
@@ -2,6 +2,8 @@ import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
2
2
  /**
3
3
  * In ARIA 1.3, `generic` role elements are additionally transparent for
4
4
  * ownership traversal.
5
+ *
6
+ * The ancestors are those of the accessibility tree: see {@link getComposedParentElement}.
5
7
  */
6
8
  export declare function getNonPresentationalAncestor(el: Element, specs: MLMLSpec, version: ARIAVersion): import("../../types/index.js").ComputedRole | {
7
9
  el: null;
@@ -1,13 +1,16 @@
1
+ import { getComposedParentElement } from '../dom/tree-boundary.js';
1
2
  import { isTransparentForOwnership } from './is-presentational.js';
2
3
  import { getComputedRole } from './get-computed-role.js';
3
4
  /**
4
5
  * In ARIA 1.3, `generic` role elements are additionally transparent for
5
6
  * ownership traversal.
7
+ *
8
+ * The ancestors are those of the accessibility tree: see {@link getComposedParentElement}.
6
9
  */
7
10
  export function getNonPresentationalAncestor(
8
11
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
9
12
  el, specs, version) {
10
- let ancestor = el.parentElement;
13
+ let ancestor = getComposedParentElement(el);
11
14
  /**
12
15
  * In ARIA 1.1 and 1.2, the role of an element is not affected by the role of its ancestor elements.
13
16
  *
@@ -19,7 +22,7 @@ el, specs, version) {
19
22
  if (!isTransparentForOwnership(ancestorRole.role?.name, version)) {
20
23
  return ancestorRole;
21
24
  }
22
- ancestor = ancestor.parentElement;
25
+ ancestor = getComposedParentElement(ancestor);
23
26
  }
24
27
  return {
25
28
  el: ancestor,
@@ -4,7 +4,9 @@ import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
4
4
  * Applies WAI-ARIA exclusion/inclusion rules, SVG rendering rules, and
5
5
  * HTML metadata element filtering.
6
6
  *
7
- * @see https://www.w3.org/TR/wai-aria-1.2/#accessibility_tree
7
+ * The ancestors it reads are those of the accessibility tree ({@link getComposedParentElement}).
8
+ *
9
+ * @see https://w3c.github.io/aria/#accessibility_tree
8
10
  *
9
11
  * @param el - The DOM element to check for accessibility tree exposure
10
12
  * @param specs - The full markup language specification
@@ -1,4 +1,5 @@
1
1
  import { ariaSpecs } from './aria-specs.js';
2
+ import { getComposedParentElement } from '../dom/tree-boundary.js';
2
3
  import { getSpecByTagName } from '../../utils/get-spec-by-tag-name.js';
3
4
  import { isPresentational } from './is-presentational.js';
4
5
  import { resolveNamespace } from '../../utils/resolve-namespace.js';
@@ -8,7 +9,9 @@ import { getComputedRole } from './get-computed-role.js';
8
9
  * Applies WAI-ARIA exclusion/inclusion rules, SVG rendering rules, and
9
10
  * HTML metadata element filtering.
10
11
  *
11
- * @see https://www.w3.org/TR/wai-aria-1.2/#accessibility_tree
12
+ * The ancestors it reads are those of the accessibility tree ({@link getComposedParentElement}).
13
+ *
14
+ * @see https://w3c.github.io/aria/#accessibility_tree
12
15
  *
13
16
  * @param el - The DOM element to check for accessibility tree exposure
14
17
  * @param specs - The full markup language specification
@@ -47,7 +50,7 @@ el, specs, version) {
47
50
  /**
48
51
  * Excluding Elements from the Accessibility Tree
49
52
  *
50
- * @see https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion
53
+ * @see https://w3c.github.io/aria/#tree_exclusion
51
54
  *
52
55
  * @param specs
53
56
  * @param el
@@ -70,7 +73,7 @@ el, specs, version) {
70
73
  if (hasDisplayNodeOrVisibilityHidden(currentEl) || currentEl.hasAttribute('hidden')) {
71
74
  return true;
72
75
  }
73
- currentEl = currentEl.parentElement;
76
+ currentEl = getComposedParentElement(currentEl);
74
77
  }
75
78
  }
76
79
  /**
@@ -97,7 +100,7 @@ el, specs, version) {
97
100
  if (currentEl.getAttribute('aria-hidden') === 'true') {
98
101
  return true;
99
102
  }
100
- currentEl = currentEl.parentElement;
103
+ currentEl = getComposedParentElement(currentEl);
101
104
  }
102
105
  }
103
106
  /**
@@ -109,13 +112,13 @@ el, specs, version) {
109
112
  * However, the text content of any excluded descendants is included.
110
113
  */
111
114
  {
112
- let currentEl = el.parentElement;
115
+ let currentEl = getComposedParentElement(el);
113
116
  while (currentEl) {
114
117
  const { role } = getComputedRole(specs, currentEl, version);
115
118
  if (role?.childrenPresentational) {
116
119
  return true;
117
120
  }
118
- currentEl = currentEl.parentElement;
121
+ currentEl = getComposedParentElement(currentEl);
119
122
  }
120
123
  }
121
124
  return false;
@@ -128,7 +131,7 @@ el, specs, version) {
128
131
  * user agents MUST provide an accessible object in the accessibility tree
129
132
  * for DOM elements that meet any of the following criteria:
130
133
  *
131
- * @see https://www.w3.org/TR/wai-aria-1.2/#tree_inclusion
134
+ * @see https://w3c.github.io/aria/#tree_inclusion
132
135
  *
133
136
  * @param specs
134
137
  * @param el
@@ -172,6 +175,11 @@ el, specs, version) {
172
175
  }
173
176
  // Has a global WAI-ARIA attribute
174
177
  for (const attr of el.attributes) {
178
+ // An attribute that the host does not output (`MLAttr#isAbsent`, e.g. `aria-label={null}`)
179
+ // is not an attribute of the DOM
180
+ if (attr.isAbsent) {
181
+ continue;
182
+ }
175
183
  if (globalAria.some(aria => aria.name === attr.localName)) {
176
184
  results.push(true);
177
185
  break;
@@ -27,9 +27,8 @@ export declare function isExposedAsPresentational(computed: ComputedRole): boole
27
27
  * - **ARIA 1.1/1.2:** Only `presentation`/`none` are transparent.
28
28
  * This matches the pre-existing behavior of `getClosestNonPresentationalDescendants`
29
29
  * and `getNonPresentationalAncestor`.
30
- * - **ARIA 1.3:** `generic` is additionally transparent per the spec:
31
- * "user agents MUST ignore any intervening elements with the role
32
- * `generic` or `none`."
30
+ * - **ARIA 1.3:** `generic` is additionally transparent per the spec, which has
31
+ * the user agent ignore the intervening elements with the role `generic` or `none`.
33
32
  *
34
33
  * **Note:** `matchesContextRole` calls this function unconditionally
35
34
  * (without a version gate) so that `presentation`/`none` are always
@@ -33,9 +33,8 @@ export function isExposedAsPresentational(computed) {
33
33
  * - **ARIA 1.1/1.2:** Only `presentation`/`none` are transparent.
34
34
  * This matches the pre-existing behavior of `getClosestNonPresentationalDescendants`
35
35
  * and `getNonPresentationalAncestor`.
36
- * - **ARIA 1.3:** `generic` is additionally transparent per the spec:
37
- * "user agents MUST ignore any intervening elements with the role
38
- * `generic` or `none`."
36
+ * - **ARIA 1.3:** `generic` is additionally transparent per the spec, which has
37
+ * the user agent ignore the intervening elements with the role `generic` or `none`.
39
38
  *
40
39
  * **Note:** `matchesContextRole` calls this function unconditionally
41
40
  * (without a version gate) so that `presentation`/`none` are always
@@ -4,11 +4,13 @@ import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
4
4
  * Context Role" in ARIA 1.2). Each condition string describes a chain of
5
5
  * ancestor roles separated by ` > ` (e.g., `"list > group"`).
6
6
  *
7
- * TODO: This function only walks the DOM `parentElement` chain and does not
7
+ * TODO: This function only walks the composed parent chain and does not
8
8
  * consider `aria-owns` relationships. An element referenced by `aria-owns` on
9
9
  * a remote ancestor should be treated as if it were an accessibility child of
10
10
  * that ancestor. Implementing this requires a document-wide reverse lookup of
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
+ *
14
+ * The chain is the one the accessibility tree is built from ({@link getComposedParentElement}).
13
15
  */
14
16
  export declare function matchesContextRole(conditions: readonly string[], ownedEl: Element, specs: MLMLSpec, version: ARIAVersion): boolean;
@@ -1,3 +1,4 @@
1
+ import { getComposedParentElement } from '../dom/tree-boundary.js';
1
2
  import { isTransparentForOwnership } from './is-presentational.js';
2
3
  import { getComputedRole } from './get-computed-role.js';
3
4
  /**
@@ -5,17 +6,19 @@ import { getComputedRole } from './get-computed-role.js';
5
6
  * Context Role" in ARIA 1.2). Each condition string describes a chain of
6
7
  * ancestor roles separated by ` > ` (e.g., `"list > group"`).
7
8
  *
8
- * TODO: This function only walks the DOM `parentElement` chain and does not
9
+ * TODO: This function only walks the composed parent chain and does not
9
10
  * consider `aria-owns` relationships. An element referenced by `aria-owns` on
10
11
  * a remote ancestor should be treated as if it were an accessibility child of
11
12
  * that ancestor. Implementing this requires a document-wide reverse lookup of
12
13
  * `aria-owns` attributes, which is a separate architectural concern.
13
14
  * See also: `has-required-owned-elements.ts` has a similar limitation.
15
+ *
16
+ * The chain is the one the accessibility tree is built from ({@link getComposedParentElement}).
14
17
  */
15
18
  export function matchesContextRole(conditions,
16
19
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
17
20
  ownedEl, specs, version) {
18
- return conditions.some(condition => matchesCondition(condition, ownedEl.parentElement, specs, version));
21
+ return conditions.some(condition => matchesCondition(condition, getComposedParentElement(ownedEl), specs, version));
19
22
  }
20
23
  function matchesCondition(condition,
21
24
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -27,9 +30,9 @@ parentEl, specs, version) {
27
30
  }
28
31
  const parentRole = getComputedRole(specs, parentEl, version, true).role;
29
32
  /**
30
- * ARIA 1.3 ("Required Accessibility Parent Role"): "To determine
31
- * whether an element has a parent with the required role, user agents
32
- * MUST ignore any elements with the role `generic` or `none`."
33
+ * ARIA 1.3 (Required Accessibility Parent Role): to determine whether an
34
+ * element has a parent with the required role, the user agent ignores the
35
+ * elements with the role `generic` or `none`.
33
36
  *
34
37
  * `presentation`/`none` are transparent in all ARIA versions to stay
35
38
  * consistent with `getNonPresentationalAncestor` (which already skips
@@ -47,14 +50,14 @@ parentEl, specs, version) {
47
50
  * @see https://w3c.github.io/aria/#scope
48
51
  */
49
52
  if (isTransparentForOwnership(parentRole?.name, version)) {
50
- parentEl = parentEl.parentElement;
53
+ parentEl = getComposedParentElement(parentEl);
51
54
  continue;
52
55
  }
53
56
  const condition = conditions.shift();
54
57
  if (condition !== parentRole?.name) {
55
58
  return false;
56
59
  }
57
- parentEl = parentEl.parentElement;
60
+ parentEl = getComposedParentElement(parentEl);
58
61
  }
59
62
  return true;
60
63
  }
@@ -0,0 +1,93 @@
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
+ /**
46
+ * What the helpers read of an element: the DOM `Element`, and `MLElement` of `@markuplint/ml-core`.
47
+ * An element without `getRootNode` (an adapter that has no notion of a tree boundary) is in one
48
+ * tree with its parent element.
49
+ */
50
+ type TreeElement = {
51
+ readonly parentElement: unknown;
52
+ getRootNode?(): unknown;
53
+ };
54
+ /**
55
+ * The parent element of an element in its own tree, which is what the DOM calls `parentElement`:
56
+ * `null` at the top of the content of a `template` element or of a shadow root, even where the
57
+ * `parentElement` of the element implementation is the `template` element.
58
+ *
59
+ * @template P - The type of the parent element the element reads
60
+ * @param el - The element to start from
61
+ * @returns The parent element, or `null` when the element is at the top of its tree
62
+ */
63
+ export declare function getTreeParentElement<P extends TreeElement>(el: {
64
+ readonly parentElement: P | null;
65
+ getRootNode?(): unknown;
66
+ }): P | null;
67
+ /**
68
+ * The parent element of an element as the accessibility tree is built: the parent element in its own
69
+ * tree, or, at the top of a shadow root, the host of the shadow root. `null` at the top of the content
70
+ * of an ordinary `template` element, and at the top of a document or of any other root that is not a
71
+ * shadow root.
72
+ *
73
+ * @template P - The type of the parent element the element reads
74
+ * @param el - The element to start from
75
+ * @returns The parent element or the shadow host, or `null` when the walk ends
76
+ */
77
+ export declare function getComposedParentElement<P extends TreeElement>(el: {
78
+ readonly parentElement: P | null;
79
+ getRootNode?(): unknown;
80
+ }): P | null;
81
+ /**
82
+ * Whether a root is a shadow root: a document fragment that has a host.
83
+ * The shadow root of `@markuplint/ml-core` is not a `ShadowRoot` of the DOM by class, and the
84
+ * content of an ordinary `template` element is a document fragment that has no host.
85
+ *
86
+ * @template P - The type of the host
87
+ * @param root - The root of a tree
88
+ * @returns `true` if the root has a host element
89
+ */
90
+ export declare function isShadowRoot<P = unknown>(root: unknown): root is {
91
+ readonly host: P;
92
+ };
93
+ export {};
@@ -0,0 +1,103 @@
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
+ /** DOM `Node.DOCUMENT_FRAGMENT_NODE` */
46
+ const DOCUMENT_FRAGMENT_NODE = 11;
47
+ /**
48
+ * The parent element of an element in its own tree, which is what the DOM calls `parentElement`:
49
+ * `null` at the top of the content of a `template` element or of a shadow root, even where the
50
+ * `parentElement` of the element implementation is the `template` element.
51
+ *
52
+ * @template P - The type of the parent element the element reads
53
+ * @param el - The element to start from
54
+ * @returns The parent element, or `null` when the element is at the top of its tree
55
+ */
56
+ export function getTreeParentElement(el) {
57
+ const parent = el.parentElement;
58
+ if (!parent) {
59
+ return null;
60
+ }
61
+ // Without a tree on either side there is no boundary between them
62
+ if (!parent.getRootNode || !el.getRootNode) {
63
+ return parent;
64
+ }
65
+ return parent.getRootNode() === el.getRootNode() ? parent : null;
66
+ }
67
+ /**
68
+ * The parent element of an element as the accessibility tree is built: the parent element in its own
69
+ * tree, or, at the top of a shadow root, the host of the shadow root. `null` at the top of the content
70
+ * of an ordinary `template` element, and at the top of a document or of any other root that is not a
71
+ * shadow root.
72
+ *
73
+ * @template P - The type of the parent element the element reads
74
+ * @param el - The element to start from
75
+ * @returns The parent element or the shadow host, or `null` when the walk ends
76
+ */
77
+ export function getComposedParentElement(el) {
78
+ const parent = getTreeParentElement(el);
79
+ if (parent) {
80
+ return parent;
81
+ }
82
+ const root = el.getRootNode?.();
83
+ if (isShadowRoot(root)) {
84
+ return root.host;
85
+ }
86
+ return null;
87
+ }
88
+ /**
89
+ * Whether a root is a shadow root: a document fragment that has a host.
90
+ * The shadow root of `@markuplint/ml-core` is not a `ShadowRoot` of the DOM by class, and the
91
+ * content of an ordinary `template` element is a document fragment that has no host.
92
+ *
93
+ * @template P - The type of the host
94
+ * @param root - The root of a tree
95
+ * @returns `true` if the root has a host element
96
+ */
97
+ export function isShadowRoot(root) {
98
+ return (typeof root === 'object' &&
99
+ root !== null &&
100
+ 'nodeType' in root &&
101
+ root.nodeType === DOCUMENT_FRAGMENT_NODE &&
102
+ 'host' in root);
103
+ }
@@ -6,9 +6,13 @@ import type { Category } from '../../types/permitted-structures.js';
6
6
  * Results are cached for repeated lookups.
7
7
  *
8
8
  * Note: entries are returned as stored in the spec's `#contentModels` map
9
- * without CSS selector parsing — they are expected to be bare tag names or
10
- * tag-with-attribute selectors (e.g. `a[href]`); complex selectors are not
11
- * decomposed into tag names.
9
+ * without CSS selector parsing. Besides bare tag names they include selectors
10
+ * (`a[href]`, `hgroup:has(h1,h2,h3,h4,h5,h6)`, `style:first-child`), which are
11
+ * not decomposed into tag names; `#custom` and `#text` stand for custom elements
12
+ * and text. An entry ending in `:first-child` holds as the first child as the HTML
13
+ * Standard counts it, text included, which markuplint's own paths match with
14
+ * `contentModelFirstChildBase` and `isContentModelFirstChild` of
15
+ * `@markuplint/selector`; matched as a plain selector, it counts elements only.
12
16
  *
13
17
  * @param contentModel - The content model category identifier
14
18
  * @param def - The specification definitions containing content model mappings