@markuplint/ml-spec 4.10.1 → 5.0.0-alpha.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 (111) hide show
  1. package/ARCHITECTURE.ja.md +267 -0
  2. package/ARCHITECTURE.md +267 -0
  3. package/CHANGELOG.md +20 -2
  4. package/README.md +6 -188
  5. package/SKILL.md +116 -0
  6. package/docs/aria-algorithms.ja.md +798 -0
  7. package/docs/aria-algorithms.md +800 -0
  8. package/docs/html-algorithms.ja.md +469 -0
  9. package/docs/html-algorithms.md +469 -0
  10. package/docs/maintenance.ja.md +331 -0
  11. package/docs/maintenance.md +331 -0
  12. package/docs/spec-resolution.ja.md +568 -0
  13. package/docs/spec-resolution.md +580 -0
  14. package/docs/type-definitions.ja.md +565 -0
  15. package/docs/type-definitions.md +565 -0
  16. package/lib/algorithm/aria/accname/__tests__/test-helpers.d.ts +45 -0
  17. package/lib/algorithm/aria/accname/__tests__/test-helpers.js +120 -0
  18. package/lib/algorithm/aria/accname/aria-steps.d.ts +51 -0
  19. package/lib/algorithm/aria/accname/aria-steps.js +104 -0
  20. package/lib/algorithm/aria/accname/compute.d.ts +51 -0
  21. package/lib/algorithm/aria/accname/compute.js +101 -0
  22. package/lib/algorithm/aria/accname/element-names.d.ts +36 -0
  23. package/lib/algorithm/aria/accname/element-names.js +342 -0
  24. package/lib/algorithm/aria/accname/helpers.d.ts +98 -0
  25. package/lib/algorithm/aria/accname/helpers.js +330 -0
  26. package/lib/algorithm/aria/accname/index.d.ts +4 -0
  27. package/lib/algorithm/aria/accname/index.js +3 -0
  28. package/lib/algorithm/aria/accname/label-steps.d.ts +25 -0
  29. package/lib/algorithm/aria/accname/label-steps.js +66 -0
  30. package/lib/algorithm/aria/accname/svg-helpers.d.ts +17 -0
  31. package/lib/algorithm/aria/accname/svg-helpers.js +30 -0
  32. package/lib/algorithm/aria/accname/types.d.ts +70 -0
  33. package/lib/algorithm/aria/accname/types.js +2 -0
  34. package/lib/algorithm/aria/accname-computation.d.ts +19 -1
  35. package/lib/algorithm/aria/accname-computation.js +136 -6
  36. package/lib/algorithm/aria/aria-specs.d.ts +8 -0
  37. package/lib/algorithm/aria/aria-specs.js +7 -0
  38. package/lib/algorithm/aria/get-aria.d.ts +12 -0
  39. package/lib/algorithm/aria/get-aria.js +42 -4
  40. package/lib/algorithm/aria/get-computed-aria-props.d.ts +22 -0
  41. package/lib/algorithm/aria/get-computed-aria-props.js +10 -0
  42. package/lib/algorithm/aria/get-computed-role.d.ts +12 -0
  43. package/lib/algorithm/aria/get-computed-role.js +118 -26
  44. package/lib/algorithm/aria/get-explicit-role.d.ts +12 -0
  45. package/lib/algorithm/aria/get-explicit-role.js +12 -0
  46. package/lib/algorithm/aria/get-implicit-role.d.ts +18 -0
  47. package/lib/algorithm/aria/get-implicit-role.js +18 -0
  48. package/lib/algorithm/aria/get-non-presentational-ancestor.d.ts +12 -0
  49. package/lib/algorithm/aria/get-non-presentational-ancestor.js +14 -2
  50. package/lib/algorithm/aria/get-permitted-roles-spec.d.ts +8 -4
  51. package/lib/algorithm/aria/get-permitted-roles-spec.js +19 -6
  52. package/lib/algorithm/aria/get-permitted-roles.d.ts +9 -0
  53. package/lib/algorithm/aria/get-permitted-roles.js +9 -0
  54. package/lib/algorithm/aria/get-role-spec.d.ts +11 -0
  55. package/lib/algorithm/aria/get-role-spec.js +21 -3
  56. package/lib/algorithm/aria/has-required-owned-elements.d.ts +24 -0
  57. package/lib/algorithm/aria/has-required-owned-elements.js +40 -14
  58. package/lib/algorithm/aria/is-exposed.d.ts +7 -4
  59. package/lib/algorithm/aria/is-exposed.js +7 -4
  60. package/lib/algorithm/aria/is-presentational.d.ts +32 -0
  61. package/lib/algorithm/aria/is-presentational.js +39 -0
  62. package/lib/algorithm/aria/matches-context-role.d.ts +19 -0
  63. package/lib/algorithm/aria/matches-context-role.js +46 -2
  64. package/lib/algorithm/html/content-model-category-to-tag-names.d.ts +9 -0
  65. package/lib/algorithm/html/content-model-category-to-tag-names.js +10 -1
  66. package/lib/algorithm/html/get-content-model.d.ts +12 -1
  67. package/lib/algorithm/html/get-content-model.js +14 -6
  68. package/lib/algorithm/html/get-selectors-by-content-model-category.d.ts +8 -0
  69. package/lib/algorithm/html/get-selectors-by-content-model-category.js +8 -0
  70. package/lib/algorithm/html/is-nothing-content-model.d.ts +7 -0
  71. package/lib/algorithm/html/is-nothing-content-model.js +7 -0
  72. package/lib/algorithm/html/is-palpable-elements.d.ts +13 -0
  73. package/lib/algorithm/html/is-palpable-elements.js +13 -0
  74. package/lib/algorithm/html/is-void-element.d.ts +9 -0
  75. package/lib/algorithm/html/is-void-element.js +9 -0
  76. package/lib/algorithm/html/may-be-focusable.d.ts +10 -0
  77. package/lib/algorithm/html/may-be-focusable.js +10 -0
  78. package/lib/const/accname.d.ts +29 -0
  79. package/lib/const/accname.js +76 -0
  80. package/lib/const/dom.d.ts +8 -0
  81. package/lib/const/dom.js +8 -0
  82. package/lib/const/index.d.ts +2 -0
  83. package/lib/const/index.js +2 -0
  84. package/lib/index.d.ts +3 -0
  85. package/lib/index.js +4 -0
  86. package/lib/types/index.d.ts +119 -0
  87. package/lib/utils/aria-version.d.ts +6 -0
  88. package/lib/utils/aria-version.js +6 -0
  89. package/lib/utils/directive-resolver.d.ts +23 -0
  90. package/lib/utils/directive-resolver.js +50 -0
  91. package/lib/utils/get-attr-specs-spec.d.ts +18 -0
  92. package/lib/utils/get-attr-specs-spec.js +22 -3
  93. package/lib/utils/get-attr-specs.d.ts +9 -0
  94. package/lib/utils/get-attr-specs.js +9 -0
  95. package/lib/utils/get-ns.d.ts +7 -0
  96. package/lib/utils/get-ns.js +7 -0
  97. package/lib/utils/get-spec-by-tag-name.d.ts +12 -1
  98. package/lib/utils/get-spec-by-tag-name.js +11 -0
  99. package/lib/utils/get-spec.d.ts +11 -1
  100. package/lib/utils/get-spec.js +10 -0
  101. package/lib/utils/merge-array.d.ts +10 -0
  102. package/lib/utils/merge-array.js +10 -0
  103. package/lib/utils/resolve-namespace.d.ts +13 -0
  104. package/lib/utils/resolve-namespace.js +10 -0
  105. package/lib/utils/resolve-version.d.ts +11 -0
  106. package/lib/utils/resolve-version.js +11 -0
  107. package/lib/utils/schema-to-spec.d.ts +7 -2
  108. package/lib/utils/schema-to-spec.js +20 -6
  109. package/lib/utils/validate-aria-version.d.ts +7 -0
  110. package/lib/utils/validate-aria-version.js +7 -0
  111. package/package.json +9 -7
@@ -1,5 +1,17 @@
1
- import { isPresentational } from './is-presentational.js';
1
+ import { isTransparentForOwnership } from './is-presentational.js';
2
2
  import { getComputedRole } from './get-computed-role.js';
3
+ /**
4
+ * Traverses the parent element chain to find the nearest ancestor with a
5
+ * non-presentational role, skipping elements that are transparent for
6
+ * ownership traversal (via `isTransparentForOwnership`).
7
+ *
8
+ * In ARIA 1.3, `generic` role elements are additionally transparent.
9
+ *
10
+ * @param el - The DOM element whose ancestors to traverse
11
+ * @param specs - The full markup language specification
12
+ * @param version - The ARIA specification version to use
13
+ * @returns The nearest non-presentational ancestor's `ComputedRole`, or `{ el: null, role: null }` if none exists
14
+ */
3
15
  export function getNonPresentationalAncestor(
4
16
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
5
17
  el, specs, version) {
@@ -12,7 +24,7 @@ el, specs, version) {
12
24
  const assumeSingleNode = version !== '1.1' && version !== '1.2';
13
25
  while (ancestor) {
14
26
  const ancestorRole = getComputedRole(specs, ancestor, version, assumeSingleNode);
15
- if (!isPresentational(ancestorRole.role?.name)) {
27
+ if (!isTransparentForOwnership(ancestorRole.role?.name, version)) {
16
28
  return ancestorRole;
17
29
  }
18
30
  ancestor = ancestor.parentElement;
@@ -1,10 +1,14 @@
1
1
  import type { ARIAVersion, Matches, MLMLSpec } from '../../types/index.js';
2
2
  /**
3
- * Getting permitted ARIA roles.
3
+ * Computes the list of permitted ARIA roles for an element at the spec level,
4
+ * operating on tag name and namespace rather than a DOM element.
4
5
  *
5
- * - If an array, it is role list.
6
- * - If `true`, this mean is "Any".
7
- * - If `false`, this mean is "No".
6
+ * @param specs - The full markup language specification
7
+ * @param localName - The element's local tag name
8
+ * @param namespace - The element's namespace URI
9
+ * @param version - The ARIA specification version to use
10
+ * @param matches - A function that tests CSS selector matches for conditional role resolution
11
+ * @returns A list of permitted roles, each with a `name` and optional `deprecated` flag
8
12
  */
9
13
  export declare function getPermittedRoles(specs: MLMLSpec, localName: string, namespace: string | null, version: ARIAVersion, matches: Matches): readonly {
10
14
  readonly name: string;
@@ -4,11 +4,15 @@ import { mergeArray } from '../../utils/merge-array.js';
4
4
  import { ariaSpecs } from './aria-specs.js';
5
5
  import { getARIA } from './get-aria.js';
6
6
  /**
7
- * Getting permitted ARIA roles.
7
+ * Computes the list of permitted ARIA roles for an element at the spec level,
8
+ * operating on tag name and namespace rather than a DOM element.
8
9
  *
9
- * - If an array, it is role list.
10
- * - If `true`, this mean is "Any".
11
- * - If `false`, this mean is "No".
10
+ * @param specs - The full markup language specification
11
+ * @param localName - The element's local tag name
12
+ * @param namespace - The element's namespace URI
13
+ * @param version - The ARIA specification version to use
14
+ * @param matches - A function that tests CSS selector matches for conditional role resolution
15
+ * @returns A list of permitted roles, each with a `name` and optional `deprecated` flag
12
16
  */
13
17
  export function getPermittedRoles(specs, localName, namespace, version, matches) {
14
18
  const aria = getARIA(specs, localName, namespace, version, matches);
@@ -16,7 +20,7 @@ export function getPermittedRoles(specs, localName, namespace, version, matches)
16
20
  return [];
17
21
  }
18
22
  const { implicitRole, permittedRoles } = aria;
19
- const { roles, graphicsRoles } = ariaSpecs(specs, version);
23
+ const { roles, graphicsRoles, dpubRoles } = ariaSpecs(specs, version);
20
24
  let permittedRoleList = [];
21
25
  if (permittedRoles === true) {
22
26
  permittedRoleList = mergeArray(permittedRoleList, roles
@@ -24,6 +28,11 @@ export function getPermittedRoles(specs, localName, namespace, version, matches)
24
28
  .map(role => ({
25
29
  name: role.name,
26
30
  })));
31
+ permittedRoleList = mergeArray(permittedRoleList, dpubRoles
32
+ .filter(role => !role.isAbstract)
33
+ .map(role => ({
34
+ name: role.name,
35
+ })));
27
36
  }
28
37
  if (isAAMInfo(permittedRoles)) {
29
38
  if (permittedRoles['core-aam']) {
@@ -54,7 +63,11 @@ export function getPermittedRoles(specs, localName, namespace, version, matches)
54
63
  if (implicitRole === false) {
55
64
  return permittedRoleList;
56
65
  }
57
- const implicitRoles = implicitRole === 'presentation' || implicitRole === 'none' ? ['none', 'presentation'] : [implicitRole];
66
+ const implicitRoles = implicitRole === 'presentation' || implicitRole === 'none'
67
+ ? ['none', 'presentation']
68
+ : version === '1.3' && (implicitRole === 'img' || implicitRole === 'image')
69
+ ? ['image', 'img']
70
+ : [implicitRole];
58
71
  return mergeArray(implicitRoles.map(r => ({ name: r })), permittedRoleList);
59
72
  }
60
73
  function isAAMInfo(permittedRoles) {
@@ -1,4 +1,13 @@
1
1
  import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
2
+ /**
3
+ * Retrieves the list of permitted ARIA roles that may be explicitly assigned
4
+ * to an element, based on its tag name, namespace, and current attribute state.
5
+ *
6
+ * @param el - The DOM element to determine permitted roles for
7
+ * @param version - The ARIA specification version to use
8
+ * @param specs - The full markup language specification
9
+ * @returns The permitted roles specification, which may be a boolean, an array of role names, or an AAM reference
10
+ */
2
11
  export declare function getPermittedRoles(el: Element, version: ARIAVersion, specs: MLMLSpec): readonly {
3
12
  readonly name: string;
4
13
  readonly deprecated?: boolean;
@@ -1,4 +1,13 @@
1
1
  import { getPermittedRoles as _getPermittedRoles } from './get-permitted-roles-spec.js';
2
+ /**
3
+ * Retrieves the list of permitted ARIA roles that may be explicitly assigned
4
+ * to an element, based on its tag name, namespace, and current attribute state.
5
+ *
6
+ * @param el - The DOM element to determine permitted roles for
7
+ * @param version - The ARIA specification version to use
8
+ * @param specs - The full markup language specification
9
+ * @returns The permitted roles specification, which may be a boolean, an array of role names, or an AAM reference
10
+ */
2
11
  export function getPermittedRoles(
3
12
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
4
13
  el, version, specs) {
@@ -1,5 +1,16 @@
1
1
  import type { ARIAVersion, ARIARoleInSchema, MLMLSpec, ARIARole } from '../../types/index.js';
2
2
  import type { NamespaceURI } from '@markuplint/ml-ast';
3
+ /**
4
+ * Retrieves the full ARIA role specification for a given role name, including
5
+ * its properties, requirements, and the complete chain of super-class roles.
6
+ * For SVG namespace elements, also searches graphics ARIA roles.
7
+ *
8
+ * @param specs - The full markup language specification
9
+ * @param roleName - The name of the ARIA role to look up
10
+ * @param namespace - The namespace URI of the element context
11
+ * @param version - The ARIA specification version to use
12
+ * @returns The role specification with super-class roles, or null if the role does not exist
13
+ */
3
14
  export declare function getRoleSpec(specs: MLMLSpec, roleName: string, namespace: NamespaceURI, version: ARIAVersion): (ARIARole & {
4
15
  superClassRoles: ARIARoleInSchema[];
5
16
  }) | null;
@@ -1,16 +1,31 @@
1
1
  import { ariaSpecs } from './aria-specs.js';
2
+ /**
3
+ * Retrieves the full ARIA role specification for a given role name, including
4
+ * its properties, requirements, and the complete chain of super-class roles.
5
+ * For SVG namespace elements, also searches graphics ARIA roles.
6
+ *
7
+ * @param specs - The full markup language specification
8
+ * @param roleName - The name of the ARIA role to look up
9
+ * @param namespace - The namespace URI of the element context
10
+ * @param version - The ARIA specification version to use
11
+ * @returns The role specification with super-class roles, or null if the role does not exist
12
+ */
2
13
  export function getRoleSpec(specs, roleName, namespace, version) {
3
14
  const role = getRoleByName(specs, roleName, namespace, version);
4
15
  if (!role) {
5
16
  return null;
6
17
  }
7
18
  const superClassRoles = recursiveTraverseSuperClassRoles(specs, roleName, namespace, version);
19
+ const requiredAccessibilityParentRole = role.requiredContextRole ?? role.requiredAccessibilityParentRole ?? [];
20
+ const allowedAccessibilityChildRoles = role.requiredOwnedElements ?? role.allowedAccessibilityChildRoles ?? [];
8
21
  return {
9
22
  name: role.name,
10
23
  isAbstract: !!role.isAbstract,
11
24
  deprecated: !!role.deprecated,
12
- requiredContextRole: role.requiredContextRole ?? [],
13
- requiredOwnedElements: role.requiredOwnedElements ?? [],
25
+ requiredAccessibilityParentRole,
26
+ allowedAccessibilityChildRoles,
27
+ requiredContextRole: requiredAccessibilityParentRole,
28
+ requiredOwnedElements: allowedAccessibilityChildRoles,
14
29
  accessibleNameRequired: !!role.accessibleNameRequired,
15
30
  accessibleNameFromAuthor: !!role.accessibleNameFromAuthor,
16
31
  accessibleNameFromContent: !!role.accessibleNameFromContent,
@@ -40,10 +55,13 @@ function getSuperClassRoles(specs, roleName, namespace, version) {
40
55
  .filter((role) => !!role) ?? null);
41
56
  }
42
57
  function getRoleByName(specs, roleName, namespace, version) {
43
- const { roles, graphicsRoles } = ariaSpecs(specs, version);
58
+ const { roles, graphicsRoles, dpubRoles } = ariaSpecs(specs, version);
44
59
  let role = roles.find(r => r.name === roleName);
45
60
  if (!role && namespace === 'http://www.w3.org/2000/svg') {
46
61
  role = graphicsRoles.find(r => r.name === roleName);
47
62
  }
63
+ if (!role) {
64
+ role = dpubRoles.find(r => r.name === roleName);
65
+ }
48
66
  return role;
49
67
  }
@@ -1,3 +1,27 @@
1
1
  import type { ARIAVersion, ComputedRole, MLMLSpec } from '../../types/index.js';
2
+ /**
3
+ * Checks whether an element satisfies the "Allowed Accessibility Child Roles"
4
+ * constraint (called "Required Owned Elements" in ARIA 1.2)
5
+ * defined by its computed ARIA role. An element satisfies this constraint if it
6
+ * has `aria-owns`, or if any of its closest non-presentational descendants match
7
+ * the required owned element roles.
8
+ *
9
+ * @param el - The DOM element to check
10
+ * @param specs - The full markup language specification
11
+ * @param version - The ARIA specification version to use
12
+ * @returns `true` if the element has no required owned elements or the constraint is satisfied, `false` otherwise
13
+ */
2
14
  export declare function hasRequiredOwnedElement(el: Element, specs: MLMLSpec, version: ARIAVersion): boolean;
15
+ /**
16
+ * Determines whether an element with a given role matches a required owned element query.
17
+ * The query string may describe a single role or a parent-child relationship using `>` notation
18
+ * (e.g., `"listitem"` or `"group > listitem"`).
19
+ *
20
+ * @param el - The DOM element to test
21
+ * @param role - The computed role of the element
22
+ * @param query - The required owned element query string, optionally containing `>` for nested requirements
23
+ * @param specs - The full markup language specification
24
+ * @param version - The ARIA specification version to use
25
+ * @returns `true` if the element and its role match the required owned element query
26
+ */
3
27
  export declare function isRequiredOwnedElement(el: Element, role: ComputedRole['role'], query: string, specs: MLMLSpec, version: ARIAVersion): boolean;
@@ -1,7 +1,19 @@
1
- import { isPresentational } from './is-presentational.js';
1
+ import { isTransparentForOwnership } from './is-presentational.js';
2
2
  import { getComputedRole } from './get-computed-role.js';
3
3
  import { getExplicitRole } from './get-explicit-role.js';
4
4
  import { getImplicitRole } from './get-implicit-role.js';
5
+ /**
6
+ * Checks whether an element satisfies the "Allowed Accessibility Child Roles"
7
+ * constraint (called "Required Owned Elements" in ARIA 1.2)
8
+ * defined by its computed ARIA role. An element satisfies this constraint if it
9
+ * has `aria-owns`, or if any of its closest non-presentational descendants match
10
+ * the required owned element roles.
11
+ *
12
+ * @param el - The DOM element to check
13
+ * @param specs - The full markup language specification
14
+ * @param version - The ARIA specification version to use
15
+ * @returns `true` if the element has no required owned elements or the constraint is satisfied, `false` otherwise
16
+ */
5
17
  export function hasRequiredOwnedElement(
6
18
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
7
19
  el, specs, version) {
@@ -18,10 +30,10 @@ el, specs, version) {
18
30
  * Otherwise, traverses descendants to find owned elements.
19
31
  */
20
32
  const computed = getComputedRole(specs, el, version);
21
- if (!computed.role || computed.role.requiredOwnedElements.length === 0) {
33
+ if (!computed.role || computed.role.allowedAccessibilityChildRoles.length === 0) {
22
34
  return true;
23
35
  }
24
- for (const expectRole of computed.role.requiredOwnedElements) {
36
+ for (const expectRole of computed.role.allowedAccessibilityChildRoles) {
25
37
  for (const owned of getClosestNonPresentationalDescendants(el, specs, version)) {
26
38
  if (isRequiredOwnedElement(owned.el, owned.role, expectRole, specs, version)) {
27
39
  return true;
@@ -30,6 +42,18 @@ el, specs, version) {
30
42
  }
31
43
  return false;
32
44
  }
45
+ /**
46
+ * Determines whether an element with a given role matches a required owned element query.
47
+ * The query string may describe a single role or a parent-child relationship using `>` notation
48
+ * (e.g., `"listitem"` or `"group > listitem"`).
49
+ *
50
+ * @param el - The DOM element to test
51
+ * @param role - The computed role of the element
52
+ * @param query - The required owned element query string, optionally containing `>` for nested requirements
53
+ * @param specs - The full markup language specification
54
+ * @param version - The ARIA specification version to use
55
+ * @returns `true` if the element and its role match the required owned element query
56
+ */
33
57
  export function isRequiredOwnedElement(
34
58
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
35
59
  el, role, query, specs, version) {
@@ -49,18 +73,20 @@ el, role, query, specs, version) {
49
73
  }
50
74
  /**
51
75
  * Gets the list of closest non-presentational descendants.
52
- * THE SPECIFICATION HAS AN ISSUE
53
- * that has not decided whether the owned element is a child or a descendant.
76
+ * This corresponds to the "Allowed Accessibility Child Roles" validation
77
+ * (called "Required Owned Elements" in ARIA 1.2).
54
78
  *
55
- * @see https://github.com/w3c/aria/issues/1033
56
- * @see https://github.com/w3c/aria/issues/748
57
- * @see https://github.com/w3c/aria/pull/1162
58
- * @see https://github.com/w3c/aria/pull/1213
79
+ * In ARIA 1.1/1.2, the spec had not decided whether the owned element
80
+ * is a child or a descendant. This implementation interprets that as
81
+ * A CHILD, but `presentation`/`none` elements are treated as transparent.
59
82
  *
60
- * Currently, this process interprets that as A CHILD
61
- * because it wants to be near to HTML semantics.
62
- * However, the presentational role behaves transparently
63
- * according to the sample code in WAI-ARIA specification.
83
+ * ARIA 1.3 formally resolves this with the definitions of
84
+ * "accessibility child" and "accessibility parent", and additionally
85
+ * makes `generic` elements transparent.
86
+ *
87
+ * @see https://w3c.github.io/aria/#mustContain
88
+ * @see https://github.com/w3c/aria/issues/1033
89
+ * @see https://github.com/w3c/aria/pull/1454
64
90
  */
65
91
  function getClosestNonPresentationalDescendants(
66
92
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -69,7 +95,7 @@ el, specs, version) {
69
95
  for (const child of el.children) {
70
96
  const explicitRole = getExplicitRole(specs, child, version);
71
97
  const computed = explicitRole.role ? explicitRole : getImplicitRole(specs, child, version);
72
- if (isPresentational(computed.role?.name)) {
98
+ if (isTransparentForOwnership(computed.role?.name, version)) {
73
99
  owned.push(...getClosestNonPresentationalDescendants(child, specs, version));
74
100
  continue;
75
101
  }
@@ -1,11 +1,14 @@
1
1
  import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
2
2
  /**
3
- * Detect including/excluding from the Accessibility Tree
3
+ * Detects whether an element is included in (exposed to) the Accessibility Tree.
4
+ * Applies WAI-ARIA exclusion/inclusion rules, SVG rendering rules, and
5
+ * HTML metadata element filtering.
4
6
  *
5
7
  * @see https://www.w3.org/TR/wai-aria-1.2/#accessibility_tree
6
8
  *
7
- * @param specs
8
- * @param el
9
- * @param version
9
+ * @param el - The DOM element to check for accessibility tree exposure
10
+ * @param specs - The full markup language specification
11
+ * @param version - The ARIA specification version to use
12
+ * @returns `true` if the element should be exposed in the accessibility tree
10
13
  */
11
14
  export declare function isExposed(el: Element, specs: MLMLSpec, version: ARIAVersion): boolean;
@@ -4,13 +4,16 @@ import { isPresentational } from './is-presentational.js';
4
4
  import { resolveNamespace } from '../../utils/resolve-namespace.js';
5
5
  import { getComputedRole } from './get-computed-role.js';
6
6
  /**
7
- * Detect including/excluding from the Accessibility Tree
7
+ * Detects whether an element is included in (exposed to) the Accessibility Tree.
8
+ * Applies WAI-ARIA exclusion/inclusion rules, SVG rendering rules, and
9
+ * HTML metadata element filtering.
8
10
  *
9
11
  * @see https://www.w3.org/TR/wai-aria-1.2/#accessibility_tree
10
12
  *
11
- * @param specs
12
- * @param el
13
- * @param version
13
+ * @param el - The DOM element to check for accessibility tree exposure
14
+ * @param specs - The full markup language specification
15
+ * @param version - The ARIA specification version to use
16
+ * @returns `true` if the element should be exposed in the accessibility tree
14
17
  */
15
18
  export function isExposed(
16
19
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -1 +1,33 @@
1
+ import type { ARIAVersion } from '../../types/index.js';
2
+ /**
3
+ * Determines whether a given role name corresponds to a presentational role
4
+ * (`"presentation"` or `"none"`), which indicates the element should be
5
+ * excluded from the accessibility tree.
6
+ *
7
+ * @param roleName - The ARIA role name to check, or undefined
8
+ * @returns `true` if the role name is `"presentation"` or `"none"`, `false` otherwise
9
+ */
1
10
  export declare function isPresentational(roleName?: string): boolean;
11
+ /**
12
+ * Determines whether a given role is transparent for ownership traversal.
13
+ *
14
+ * **Version behavior:**
15
+ * - **ARIA 1.1/1.2:** Only `presentation`/`none` are transparent.
16
+ * This matches the pre-existing behavior of `getClosestNonPresentationalDescendants`
17
+ * and `getNonPresentationalAncestor`.
18
+ * - **ARIA 1.3:** `generic` is additionally transparent per the spec:
19
+ * "user agents MUST ignore any intervening elements with the role
20
+ * `generic` or `none`."
21
+ *
22
+ * **Note:** `matchesContextRole` calls this function unconditionally
23
+ * (without a version gate) so that `presentation`/`none` are always
24
+ * transparent, consistent with `getNonPresentationalAncestor`.
25
+ *
26
+ * @see https://w3c.github.io/aria/#mustContain — "Allowed Accessibility Child Roles" (called "Required Owned Elements" in ARIA 1.2)
27
+ * @see https://w3c.github.io/aria/#scope — "Required Accessibility Parent Role" (called "Required Context Role" in ARIA 1.2)
28
+ *
29
+ * @param roleName - The ARIA role name to check, or undefined
30
+ * @param version - The ARIA specification version
31
+ * @returns `true` if the role should be skipped during ownership traversal
32
+ */
33
+ export declare function isTransparentForOwnership(roleName: string | undefined, version: ARIAVersion): boolean;
@@ -1,6 +1,45 @@
1
+ /**
2
+ * Determines whether a given role name corresponds to a presentational role
3
+ * (`"presentation"` or `"none"`), which indicates the element should be
4
+ * excluded from the accessibility tree.
5
+ *
6
+ * @param roleName - The ARIA role name to check, or undefined
7
+ * @returns `true` if the role name is `"presentation"` or `"none"`, `false` otherwise
8
+ */
1
9
  export function isPresentational(roleName) {
2
10
  if (!roleName) {
3
11
  return false;
4
12
  }
5
13
  return ['presentation', 'none'].includes(roleName);
6
14
  }
15
+ /**
16
+ * Determines whether a given role is transparent for ownership traversal.
17
+ *
18
+ * **Version behavior:**
19
+ * - **ARIA 1.1/1.2:** Only `presentation`/`none` are transparent.
20
+ * This matches the pre-existing behavior of `getClosestNonPresentationalDescendants`
21
+ * and `getNonPresentationalAncestor`.
22
+ * - **ARIA 1.3:** `generic` is additionally transparent per the spec:
23
+ * "user agents MUST ignore any intervening elements with the role
24
+ * `generic` or `none`."
25
+ *
26
+ * **Note:** `matchesContextRole` calls this function unconditionally
27
+ * (without a version gate) so that `presentation`/`none` are always
28
+ * transparent, consistent with `getNonPresentationalAncestor`.
29
+ *
30
+ * @see https://w3c.github.io/aria/#mustContain — "Allowed Accessibility Child Roles" (called "Required Owned Elements" in ARIA 1.2)
31
+ * @see https://w3c.github.io/aria/#scope — "Required Accessibility Parent Role" (called "Required Context Role" in ARIA 1.2)
32
+ *
33
+ * @param roleName - The ARIA role name to check, or undefined
34
+ * @param version - The ARIA specification version
35
+ * @returns `true` if the role should be skipped during ownership traversal
36
+ */
37
+ export function isTransparentForOwnership(roleName, version) {
38
+ if (isPresentational(roleName)) {
39
+ return true;
40
+ }
41
+ if (version === '1.3' && roleName === 'generic') {
42
+ return true;
43
+ }
44
+ return false;
45
+ }
@@ -1,2 +1,21 @@
1
1
  import type { ARIAVersion, MLMLSpec } from '../../types/index.js';
2
+ /**
3
+ * Checks whether an element's parent hierarchy satisfies the
4
+ * "Required Accessibility Parent Role" (called "Required Context Role" in ARIA 1.2).
5
+ * Each condition string may describe a chain of ancestor roles separated by
6
+ * ` > ` (e.g., `"list > group"`).
7
+ *
8
+ * TODO: This function only walks the DOM `parentElement` chain and does not
9
+ * consider `aria-owns` relationships. An element referenced by `aria-owns` on
10
+ * a remote ancestor should be treated as if it were an accessibility child of
11
+ * that ancestor. Implementing this requires a document-wide reverse lookup of
12
+ * `aria-owns` attributes, which is a separate architectural concern.
13
+ * See also: `has-required-owned-elements.ts` has a similar limitation.
14
+ *
15
+ * @param conditions - An array of required accessibility parent role condition strings to match against
16
+ * @param ownedEl - The owned DOM element whose parent context is being validated
17
+ * @param specs - The full markup language specification
18
+ * @param version - The ARIA specification version to use
19
+ * @returns `true` if any of the context role conditions are satisfied by the element's ancestors
20
+ */
2
21
  export declare function matchesContextRole(conditions: readonly string[], ownedEl: Element, specs: MLMLSpec, version: ARIAVersion): boolean;
@@ -1,4 +1,24 @@
1
+ import { isTransparentForOwnership } from './is-presentational.js';
1
2
  import { getComputedRole } from './get-computed-role.js';
3
+ /**
4
+ * Checks whether an element's parent hierarchy satisfies the
5
+ * "Required Accessibility Parent Role" (called "Required Context Role" in ARIA 1.2).
6
+ * Each condition string may describe a chain of ancestor roles separated by
7
+ * ` > ` (e.g., `"list > group"`).
8
+ *
9
+ * TODO: This function only walks the DOM `parentElement` chain and does not
10
+ * consider `aria-owns` relationships. An element referenced by `aria-owns` on
11
+ * a remote ancestor should be treated as if it were an accessibility child of
12
+ * that ancestor. Implementing this requires a document-wide reverse lookup of
13
+ * `aria-owns` attributes, which is a separate architectural concern.
14
+ * See also: `has-required-owned-elements.ts` has a similar limitation.
15
+ *
16
+ * @param conditions - An array of required accessibility parent role condition strings to match against
17
+ * @param ownedEl - The owned DOM element whose parent context is being validated
18
+ * @param specs - The full markup language specification
19
+ * @param version - The ARIA specification version to use
20
+ * @returns `true` if any of the context role conditions are satisfied by the element's ancestors
21
+ */
2
22
  export function matchesContextRole(conditions,
3
23
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
4
24
  ownedEl, specs, version) {
@@ -7,13 +27,37 @@ ownedEl, specs, version) {
7
27
  function matchesCondition(condition,
8
28
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
9
29
  parentEl, specs, version) {
10
- const conditions = condition.split(/\s+>\s+/).reverse();
30
+ const conditions = condition.split(/\s+>\s+/).toReversed();
11
31
  while (conditions.length > 0) {
12
32
  if (!parentEl) {
13
33
  return false;
14
34
  }
15
- const condition = conditions.shift();
16
35
  const parentRole = getComputedRole(specs, parentEl, version, true).role;
36
+ /**
37
+ * ARIA 1.3 ("Required Accessibility Parent Role"): "To determine
38
+ * whether an element has a parent with the required role, user agents
39
+ * MUST ignore any elements with the role `generic` or `none`."
40
+ *
41
+ * `presentation`/`none` are transparent in all ARIA versions to stay
42
+ * consistent with `getNonPresentationalAncestor` (which already skips
43
+ * them unconditionally). `generic` is additionally transparent in 1.3+
44
+ * per the spec.
45
+ *
46
+ * While the ARIA 1.1/1.2 specification text does not explicitly define
47
+ * this transparency for the context role check, applying it is a
48
+ * pragmatic choice: `getNonPresentationalAncestor` and `classifyChildren`
49
+ * (for owned elements) already skip `presentation`/`none` in all versions.
50
+ * Treating the context role check differently would create inconsistent
51
+ * behavior where a structure is valid from the parent's perspective
52
+ * (owned elements) but invalid from the child's perspective (context role).
53
+ *
54
+ * @see https://w3c.github.io/aria/#scope
55
+ */
56
+ if (isTransparentForOwnership(parentRole?.name, version)) {
57
+ parentEl = parentEl.parentElement;
58
+ continue;
59
+ }
60
+ const condition = conditions.shift();
17
61
  if (condition !== parentRole?.name) {
18
62
  return false;
19
63
  }
@@ -1,3 +1,12 @@
1
1
  import type { MLMLSpec } from '../../types/index.js';
2
2
  import type { Category } from '../../types/permitted-structures.js';
3
+ /**
4
+ * Converts a content model category (e.g., `#flow`, `#phrasing`) to a sorted,
5
+ * frozen array of HTML/SVG tag names that belong to that category.
6
+ * Results are cached for repeated lookups.
7
+ *
8
+ * @param contentModel - The content model category identifier
9
+ * @param def - The specification definitions containing content model mappings
10
+ * @returns A frozen, sorted array of tag name strings belonging to the category
11
+ */
3
12
  export declare function contentModelCategoryToTagNames(contentModel: Category, def: MLMLSpec['def']): ReadonlyArray<string>;
@@ -1,11 +1,20 @@
1
1
  const cache = new Map();
2
+ /**
3
+ * Converts a content model category (e.g., `#flow`, `#phrasing`) to a sorted,
4
+ * frozen array of HTML/SVG tag names that belong to that category.
5
+ * Results are cached for repeated lookups.
6
+ *
7
+ * @param contentModel - The content model category identifier
8
+ * @param def - The specification definitions containing content model mappings
9
+ * @returns A frozen, sorted array of tag name strings belonging to the category
10
+ */
2
11
  export function contentModelCategoryToTagNames(contentModel, def) {
3
12
  const cached = cache.get(contentModel);
4
13
  if (cached) {
5
14
  return cached;
6
15
  }
7
16
  const tags = def['#contentModels'][contentModel];
8
- const sortedTag = Object.freeze(tags && Array.isArray(tags) ? tags.sort() : []);
17
+ const sortedTag = Object.freeze(tags && Array.isArray(tags) ? tags.toSorted() : []);
9
18
  cache.set(contentModel, sortedTag);
10
19
  return sortedTag;
11
20
  }
@@ -1,4 +1,15 @@
1
1
  import type { ElementSpec } from '../../types/index.js';
2
+ import type { PermittedContentPattern } from '../../types/permitted-structures.js';
3
+ import type { ReadonlyDeep } from 'type-fest';
2
4
  type Specs = readonly Pick<ElementSpec, 'name' | 'contentModel'>[];
3
- export declare function getContentModel(el: Element, specs: Specs): boolean | readonly (import("type-fest/source/readonly-deep.js").ReadonlyObjectDeep<import("../../types/permitted-structures.js").PermittedContentRequire> | import("type-fest/source/readonly-deep.js").ReadonlyObjectDeep<import("../../types/permitted-structures.js").PermittedContentOptional> | import("type-fest/source/readonly-deep.js").ReadonlyObjectDeep<import("../../types/permitted-structures.js").PermittedContentOneOrMore> | import("type-fest/source/readonly-deep.js").ReadonlyObjectDeep<import("../../types/permitted-structures.js").PermittedContentZeroOrMore> | import("type-fest/source/readonly-deep.js").ReadonlyObjectDeep<import("../../types/permitted-structures.js").PermittedContentChoice> | import("type-fest/source/readonly-deep.js").ReadonlyObjectDeep<import("../../types/permitted-structures.js").PermittedContentTransparent>)[] | null;
5
+ /**
6
+ * Retrieves the permitted content model for an element. Evaluates any conditional
7
+ * content models based on the element's current attributes (e.g., different content
8
+ * models for `<ol>` vs `<ol reversed>`). Results are cached per element.
9
+ *
10
+ * @param el - The DOM element to retrieve the content model for
11
+ * @param specs - The element specifications containing content model definitions
12
+ * @returns The permitted content patterns array, a boolean (true for any content, false for no content), or null if no spec exists
13
+ */
14
+ export declare function getContentModel(el: Element, specs: Specs): ReadonlyDeep<PermittedContentPattern[]> | boolean | null;
4
15
  export {};
@@ -1,25 +1,33 @@
1
1
  import { getSpec } from '../../utils/get-spec.js';
2
- const cachesBySpecs = new Map();
2
+ const contentModelCache = new WeakMap();
3
+ /**
4
+ * Retrieves the permitted content model for an element. Evaluates any conditional
5
+ * content models based on the element's current attributes (e.g., different content
6
+ * models for `<ol>` vs `<ol reversed>`). Results are cached per element.
7
+ *
8
+ * @param el - The DOM element to retrieve the content model for
9
+ * @param specs - The element specifications containing content model definitions
10
+ * @returns The permitted content patterns array, a boolean (true for any content, false for no content), or null if no spec exists
11
+ */
3
12
  export function getContentModel(
4
13
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
5
14
  el, specs) {
6
- const cacheByEl = cachesBySpecs.get(specs) ?? new Map();
7
- const cached = cacheByEl.get(el);
15
+ const cached = contentModelCache.get(el);
8
16
  if (cached !== undefined) {
9
17
  return cached;
10
18
  }
11
19
  const spec = getSpec(el, specs);
12
20
  if (!spec) {
13
- cacheByEl.set(el, null);
21
+ contentModelCache.set(el, null);
14
22
  return null;
15
23
  }
16
24
  const conditions = spec.contentModel.conditional ?? [];
17
25
  for (const cond of conditions) {
18
26
  if (el.matches(cond.condition)) {
19
- cacheByEl.set(el, cond.contents);
27
+ contentModelCache.set(el, cond.contents);
20
28
  return cond.contents;
21
29
  }
22
30
  }
23
- cacheByEl.set(el, spec.contentModel.contents);
31
+ contentModelCache.set(el, spec.contentModel.contents);
24
32
  return spec.contentModel.contents;
25
33
  }
@@ -1,3 +1,11 @@
1
1
  import type { MLMLSpec } from '../../types/index.js';
2
2
  import type { Category } from '../../types/permitted-structures.js';
3
+ /**
4
+ * Retrieves the CSS selectors associated with a content model category from the spec definitions.
5
+ * These selectors can be used to match elements that belong to the given category.
6
+ *
7
+ * @param specs - The full markup language specification
8
+ * @param category - The content model category identifier (e.g., `#flow`, `#interactive`)
9
+ * @returns A readonly array of CSS selector strings for the category, or an empty array if the category is not defined
10
+ */
3
11
  export declare function getSelectorsByContentModelCategory(specs: MLMLSpec, category: Category): ReadonlyArray<string>;