@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
@@ -0,0 +1,120 @@
1
+ /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
2
+ import { ELEMENT_NODE, TEXT_NODE, XHTML_NAMESPACE, EMBEDDED_CONTROL_ROLES, isNativeEmbeddedControl, } from '../../../../const/index.js';
3
+ /**
4
+ * Creates a test AccnameElement (plain object).
5
+ *
6
+ * **Note:** `textContent` is computed eagerly at creation time from the initial
7
+ * `children`. Adding children after creation will NOT update `textContent`.
8
+ * Build the full child tree before calling this function.
9
+ *
10
+ * @param localName - The local tag name of the element
11
+ * @param options - Configuration for attributes, children, parent, and namespace
12
+ * @returns A plain object implementing the AccnameElement interface
13
+ */
14
+ export function element(localName, options = {}) {
15
+ const attrs = options.attrs ?? {};
16
+ const childNodes = options.children ?? [];
17
+ const elementChildren = [];
18
+ for (const child of childNodes) {
19
+ if (child.nodeType === ELEMENT_NODE) {
20
+ elementChildren.push(child);
21
+ }
22
+ }
23
+ const el = {
24
+ nodeType: ELEMENT_NODE,
25
+ localName,
26
+ id: attrs['id'] ?? '',
27
+ namespaceURI: options.namespaceURI ?? XHTML_NAMESPACE,
28
+ textContent: computeTextContent(childNodes),
29
+ parentElement: options.parentElement ?? null,
30
+ children: elementChildren,
31
+ childNodes,
32
+ getAttribute(name) {
33
+ return attrs[name] ?? null;
34
+ },
35
+ hasAttribute(name) {
36
+ return name in attrs;
37
+ },
38
+ };
39
+ return el;
40
+ }
41
+ /**
42
+ * Creates a test text node.
43
+ *
44
+ * @param text - The text content of the node
45
+ * @returns A plain object implementing the AccnameNode interface
46
+ */
47
+ export function textNode(text) {
48
+ return {
49
+ nodeType: TEXT_NODE,
50
+ textContent: text,
51
+ };
52
+ }
53
+ function computeTextContent(childNodes) {
54
+ return childNodes
55
+ .map(child => {
56
+ if (child.nodeType === TEXT_NODE) {
57
+ return child.textContent ?? '';
58
+ }
59
+ if (child.nodeType === ELEMENT_NODE) {
60
+ return child.textContent ?? '';
61
+ }
62
+ return '';
63
+ })
64
+ .join('');
65
+ }
66
+ /**
67
+ * Creates a test AccnameResolver.
68
+ *
69
+ * @param options - Configuration for element lookups, label associations, and behavior overrides
70
+ * @returns A resolver implementing the AccnameResolver interface for testing
71
+ */
72
+ export function createTestResolver(options = {}) {
73
+ const elements = options.elements ?? new Map();
74
+ const labels = options.labels ?? new Map();
75
+ const nameFromContent = options.nameFromContent ?? new Set();
76
+ const hiddenIds = options.hiddenIds ?? new Set();
77
+ const embeddedControlRoles = options.embeddedControlRoles ?? EMBEDDED_CONTROL_ROLES;
78
+ return {
79
+ getElementById(id) {
80
+ return elements.get(id) ?? null;
81
+ },
82
+ getLabelsForId(id) {
83
+ return labels.get(id) ?? [];
84
+ },
85
+ allowsNameFromContent(el) {
86
+ if (options.allowsNameFromContentFn) {
87
+ return options.allowsNameFromContentFn(el);
88
+ }
89
+ return nameFromContent.has(el.localName);
90
+ },
91
+ isHidden(el) {
92
+ if (options.isHiddenFn) {
93
+ return options.isHiddenFn(el);
94
+ }
95
+ if (hiddenIds.has(el.id)) {
96
+ return true;
97
+ }
98
+ if (el.getAttribute('aria-hidden') === 'true') {
99
+ return true;
100
+ }
101
+ if (el.hasAttribute('hidden')) {
102
+ return true;
103
+ }
104
+ return false;
105
+ },
106
+ isEmbeddedControl(el) {
107
+ if (options.isEmbeddedControlFn) {
108
+ return options.isEmbeddedControlFn(el);
109
+ }
110
+ // Check explicit role
111
+ const role = el.getAttribute('role')?.trim().split(/\s+/)[0];
112
+ if (role && embeddedControlRoles.has(role)) {
113
+ return true;
114
+ }
115
+ // Check native HTML elements
116
+ return isNativeEmbeddedControl(el);
117
+ },
118
+ ...(options.getPrecomputedNameFn ? { getPrecomputedName: options.getPrecomputedNameFn } : {}),
119
+ };
120
+ }
@@ -0,0 +1,51 @@
1
+ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
2
+ /**
3
+ * Resolves accessible name via `aria-labelledby` attribute.
4
+ *
5
+ * Implements AccName 1.2 §4.3.2 Step 2B:
6
+ * "If the current node has an `aria-labelledby` attribute that contains
7
+ * at least one valid IDREF, and the current node is not already part of
8
+ * an ongoing `aria-labelledby` traversal, process its IDREFs [...]"
9
+ *
10
+ * Control flow:
11
+ * 1. Read `aria-labelledby` attribute; return null if absent or empty.
12
+ * 2. Split the attribute value by whitespace into IDREF tokens.
13
+ * 3. Mark the current element's ID as visited (cycle prevention for A→B→A).
14
+ * 4. For each IDREF:
15
+ * a. Skip if already visited — **except** self-references (see below).
16
+ * b. Resolve the referenced element via `resolver.getElementById`.
17
+ * c. Recursively compute the referenced element's name with
18
+ * `inLabelledbyTraversal=true` (prevents Step 2B re-entry in `compute.ts`).
19
+ * d. Each IDREF branch gets its own copy of the visited set so that
20
+ * one branch's traversal does not block later branches.
21
+ * 5. Join all resolved parts with a space separator and flatten whitespace.
22
+ *
23
+ * **Self-reference handling** (spec-defined, not a custom extension):
24
+ * An element may reference its own ID in `aria-labelledby` to include its
25
+ * own content alongside other referenced elements. This is explicitly
26
+ * permitted by the spec (AccName 1.2 §4.3.2 Example 2):
27
+ * `<h2 id="h" aria-labelledby="h foo">Meeting</h2>`
28
+ * Infinite recursion is prevented by `inLabelledbyTraversal`, not by the
29
+ * visited set — `computeFn` is called with `inLabelledbyTraversal=true`,
30
+ * which causes `compute.ts` to skip Step 2B on the referenced element.
31
+ *
32
+ * @param el - The element with a potential aria-labelledby attribute
33
+ * @param resolver - Environment-dependent resolver for element lookups
34
+ * @param visited - Set of element IDs already visited (cycle prevention)
35
+ * @param computeFn - The recursive accessible name computation function
36
+ * @returns The resolved name result, or null if aria-labelledby is not present or yields no name
37
+ * @see https://www.w3.org/TR/accname-1.2/#comp_labelledby_traversal — AccName 1.2 §4.3.2 Step 2B
38
+ */
39
+ export declare function resolveAriaLabelledby(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult): AccnameResult | null;
40
+ /**
41
+ * Resolves accessible name via `aria-label` attribute.
42
+ *
43
+ * Implements AccName 1.2 §4.3.2 Step 2D:
44
+ * "If the current node has an `aria-label` attribute whose value is
45
+ * not undefined, not the empty string, and not a string of whitespace [...]"
46
+ *
47
+ * @param el - The element with a potential aria-label attribute
48
+ * @returns The resolved name result, or null if aria-label is not present or empty
49
+ * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2D
50
+ */
51
+ export declare function resolveAriaLabel(el: AccnameElement): AccnameResult | null;
@@ -0,0 +1,104 @@
1
+ /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
2
+ import { flattenText, makeResult } from './helpers.js';
3
+ /**
4
+ * Resolves accessible name via `aria-labelledby` attribute.
5
+ *
6
+ * Implements AccName 1.2 §4.3.2 Step 2B:
7
+ * "If the current node has an `aria-labelledby` attribute that contains
8
+ * at least one valid IDREF, and the current node is not already part of
9
+ * an ongoing `aria-labelledby` traversal, process its IDREFs [...]"
10
+ *
11
+ * Control flow:
12
+ * 1. Read `aria-labelledby` attribute; return null if absent or empty.
13
+ * 2. Split the attribute value by whitespace into IDREF tokens.
14
+ * 3. Mark the current element's ID as visited (cycle prevention for A→B→A).
15
+ * 4. For each IDREF:
16
+ * a. Skip if already visited — **except** self-references (see below).
17
+ * b. Resolve the referenced element via `resolver.getElementById`.
18
+ * c. Recursively compute the referenced element's name with
19
+ * `inLabelledbyTraversal=true` (prevents Step 2B re-entry in `compute.ts`).
20
+ * d. Each IDREF branch gets its own copy of the visited set so that
21
+ * one branch's traversal does not block later branches.
22
+ * 5. Join all resolved parts with a space separator and flatten whitespace.
23
+ *
24
+ * **Self-reference handling** (spec-defined, not a custom extension):
25
+ * An element may reference its own ID in `aria-labelledby` to include its
26
+ * own content alongside other referenced elements. This is explicitly
27
+ * permitted by the spec (AccName 1.2 §4.3.2 Example 2):
28
+ * `<h2 id="h" aria-labelledby="h foo">Meeting</h2>`
29
+ * Infinite recursion is prevented by `inLabelledbyTraversal`, not by the
30
+ * visited set — `computeFn` is called with `inLabelledbyTraversal=true`,
31
+ * which causes `compute.ts` to skip Step 2B on the referenced element.
32
+ *
33
+ * @param el - The element with a potential aria-labelledby attribute
34
+ * @param resolver - Environment-dependent resolver for element lookups
35
+ * @param visited - Set of element IDs already visited (cycle prevention)
36
+ * @param computeFn - The recursive accessible name computation function
37
+ * @returns The resolved name result, or null if aria-labelledby is not present or yields no name
38
+ * @see https://www.w3.org/TR/accname-1.2/#comp_labelledby_traversal — AccName 1.2 §4.3.2 Step 2B
39
+ */
40
+ export function resolveAriaLabelledby(el, resolver, visited, computeFn) {
41
+ const labelledbyAttr = el.getAttribute('aria-labelledby');
42
+ if (!labelledbyAttr?.trim()) {
43
+ return null;
44
+ }
45
+ const ids = labelledbyAttr.trim().split(/\s+/);
46
+ const parts = [];
47
+ const newVisited = new Set(visited);
48
+ // Mark the current element as visited to prevent other elements from
49
+ // circling back to it (e.g., A → B → A).
50
+ if (el.id) {
51
+ newVisited.add(el.id);
52
+ }
53
+ for (const id of ids) {
54
+ // Skip IDs already visited — but allow self-references (id === el.id).
55
+ // The spec requires that an element CAN reference itself in
56
+ // aria-labelledby to include its own content alongside other IDs.
57
+ // Example (AccName 1.2 §4.3.2 Example 2):
58
+ // <h2 id="h" aria-labelledby="h foo">Meeting</h2>
59
+ // Here "h" references itself, contributing "Meeting" to the result.
60
+ //
61
+ // Infinite recursion is prevented not by the visited set, but by
62
+ // inLabelledbyTraversal: computeFn is called with true below,
63
+ // which causes compute.ts to skip Step 2B (aria-labelledby) on
64
+ // the referenced element, so it never re-enters this function.
65
+ if (newVisited.has(id) && id !== el.id) {
66
+ continue;
67
+ }
68
+ const referenced = resolver.getElementById(id);
69
+ if (!referenced) {
70
+ continue;
71
+ }
72
+ // Each branch gets its own visited set so that one IDREF's
73
+ // traversal doesn't block resolution of a later IDREF.
74
+ const innerVisited = new Set(newVisited);
75
+ innerVisited.add(id);
76
+ const result = computeFn(referenced, resolver, true, innerVisited);
77
+ if (result.name) {
78
+ parts.push(result.name);
79
+ }
80
+ }
81
+ const name = flattenText(parts.join(' '));
82
+ if (name) {
83
+ return makeResult(name, 'aria-labelledby');
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * Resolves accessible name via `aria-label` attribute.
89
+ *
90
+ * Implements AccName 1.2 §4.3.2 Step 2D:
91
+ * "If the current node has an `aria-label` attribute whose value is
92
+ * not undefined, not the empty string, and not a string of whitespace [...]"
93
+ *
94
+ * @param el - The element with a potential aria-label attribute
95
+ * @returns The resolved name result, or null if aria-label is not present or empty
96
+ * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2D
97
+ */
98
+ export function resolveAriaLabel(el) {
99
+ const label = el.getAttribute('aria-label');
100
+ if (label?.trim()) {
101
+ return makeResult(label, 'aria-label');
102
+ }
103
+ return null;
104
+ }
@@ -0,0 +1,51 @@
1
+ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
2
+ /**
3
+ * Computes the accessible name for an element.
4
+ *
5
+ * Implements the Accessible Name and Description Computation algorithm
6
+ * per AccName 1.2 §4.3.2 ("Computation Steps") and HTML-AAM §4.1
7
+ * ("Accessible Name and Description Computation").
8
+ *
9
+ * Control flow (Steps map to AccName 1.2 §4.3.2):
10
+ *
11
+ * 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`) and NOT
12
+ * referenced by `aria-labelledby`, return empty immediately.
13
+ * 2. **Pre-computed name** — (Implementation-specific extension) If the
14
+ * resolver provides `getPrecomputedName`, check it before standard steps.
15
+ * Used by ml-core's Pretender integration for framework components.
16
+ * 3. **Step 2B — `aria-labelledby`** — Resolve referenced elements and
17
+ * recursively compute their names. Skipped when already inside a
18
+ * labelledby traversal (`inLabelledbyTraversal`) to prevent re-entry.
19
+ * 4. **Step 2D — `aria-label`** — Use the `aria-label` attribute value.
20
+ * 5. **Step 2E — Element-specific name** — Dispatch to HTML-AAM §4.1
21
+ * element-specific rules (label association, alt, value, legend, caption, SVG title).
22
+ * 6. **Step 2F — Name from content** — If the element's role allows
23
+ * `nameFrom: ["content"]`, or the element is referenced by `aria-labelledby`
24
+ * (`inLabelledbyTraversal`), recursively collect child text (including
25
+ * embedded control values per Step 2C).
26
+ * 7. **Step 2I — Title fallback** — Use the `title` attribute value.
27
+ *
28
+ * @param el - The element to compute the accessible name for
29
+ * @param resolver - Environment-dependent resolver for DOM traversal and role queries
30
+ * @returns The computed name and its source
31
+ * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
32
+ * @see https://www.w3.org/TR/html-aam-1.0/#accessible-name-and-description-computation — HTML-AAM §4.1
33
+ */
34
+ export declare function computeAccessibleName(el: AccnameElement, resolver: AccnameResolver): AccnameResult;
35
+ /**
36
+ * Internal recursive entry point for accessible name computation.
37
+ *
38
+ * This function is the recursive core called by `resolveAriaLabelledby` (Step 2B),
39
+ * `resolveLabelText` (Step 2E), and `resolveNameFromContent` (Step 2F) when they
40
+ * need to compute a child or referenced element's name.
41
+ *
42
+ * @param el - The element to compute the accessible name for
43
+ * @param resolver - Environment-dependent resolver for DOM traversal and role queries
44
+ * @param inLabelledbyTraversal - When true, Step 2B is skipped to prevent re-entry
45
+ * into `resolveAriaLabelledby`, and Step 2F name-from-content is enabled regardless
46
+ * of role. This is set by Step 2B when processing each IDREF.
47
+ * @param visited - Set of element IDs already visited (cycle prevention for aria-labelledby)
48
+ * @returns The computed name and its source
49
+ * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
50
+ */
51
+ export declare function computeAccessibleNameInternal(el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>): AccnameResult;
@@ -0,0 +1,101 @@
1
+ /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- AccnameElement wraps mutable DOM types */
2
+ import { resolveAriaLabel, resolveAriaLabelledby } from './aria-steps.js';
3
+ import { getElementSpecificName } from './element-names.js';
4
+ import { makeResult, resolveNameFromContent } from './helpers.js';
5
+ const EMPTY_RESULT = { name: '', source: null };
6
+ /**
7
+ * Computes the accessible name for an element.
8
+ *
9
+ * Implements the Accessible Name and Description Computation algorithm
10
+ * per AccName 1.2 §4.3.2 ("Computation Steps") and HTML-AAM §4.1
11
+ * ("Accessible Name and Description Computation").
12
+ *
13
+ * Control flow (Steps map to AccName 1.2 §4.3.2):
14
+ *
15
+ * 1. **Step 2A — Hidden check** — If the element is hidden (per `isHidden`) and NOT
16
+ * referenced by `aria-labelledby`, return empty immediately.
17
+ * 2. **Pre-computed name** — (Implementation-specific extension) If the
18
+ * resolver provides `getPrecomputedName`, check it before standard steps.
19
+ * Used by ml-core's Pretender integration for framework components.
20
+ * 3. **Step 2B — `aria-labelledby`** — Resolve referenced elements and
21
+ * recursively compute their names. Skipped when already inside a
22
+ * labelledby traversal (`inLabelledbyTraversal`) to prevent re-entry.
23
+ * 4. **Step 2D — `aria-label`** — Use the `aria-label` attribute value.
24
+ * 5. **Step 2E — Element-specific name** — Dispatch to HTML-AAM §4.1
25
+ * element-specific rules (label association, alt, value, legend, caption, SVG title).
26
+ * 6. **Step 2F — Name from content** — If the element's role allows
27
+ * `nameFrom: ["content"]`, or the element is referenced by `aria-labelledby`
28
+ * (`inLabelledbyTraversal`), recursively collect child text (including
29
+ * embedded control values per Step 2C).
30
+ * 7. **Step 2I — Title fallback** — Use the `title` attribute value.
31
+ *
32
+ * @param el - The element to compute the accessible name for
33
+ * @param resolver - Environment-dependent resolver for DOM traversal and role queries
34
+ * @returns The computed name and its source
35
+ * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
36
+ * @see https://www.w3.org/TR/html-aam-1.0/#accessible-name-and-description-computation — HTML-AAM §4.1
37
+ */
38
+ export function computeAccessibleName(el, resolver) {
39
+ return computeAccessibleNameInternal(el, resolver, false, new Set());
40
+ }
41
+ /**
42
+ * Internal recursive entry point for accessible name computation.
43
+ *
44
+ * This function is the recursive core called by `resolveAriaLabelledby` (Step 2B),
45
+ * `resolveLabelText` (Step 2E), and `resolveNameFromContent` (Step 2F) when they
46
+ * need to compute a child or referenced element's name.
47
+ *
48
+ * @param el - The element to compute the accessible name for
49
+ * @param resolver - Environment-dependent resolver for DOM traversal and role queries
50
+ * @param inLabelledbyTraversal - When true, Step 2B is skipped to prevent re-entry
51
+ * into `resolveAriaLabelledby`, and Step 2F name-from-content is enabled regardless
52
+ * of role. This is set by Step 2B when processing each IDREF.
53
+ * @param visited - Set of element IDs already visited (cycle prevention for aria-labelledby)
54
+ * @returns The computed name and its source
55
+ * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2
56
+ */
57
+ export function computeAccessibleNameInternal(el, resolver, inLabelledbyTraversal, visited) {
58
+ // AccName 1.2 §4.3.2 Step 2A: Hidden elements return empty unless referenced by aria-labelledby
59
+ if (!inLabelledbyTraversal && resolver.isHidden(el)) {
60
+ return EMPTY_RESULT;
61
+ }
62
+ // [Implementation-specific] Pre-computed name (e.g., Pretender integration) — checked after hidden check
63
+ if (resolver.getPrecomputedName) {
64
+ const precomputed = resolver.getPrecomputedName(el);
65
+ if (precomputed != null) {
66
+ return makeResult(precomputed, 'content');
67
+ }
68
+ }
69
+ // AccName 1.2 §4.3.2 Step 2B: aria-labelledby (skipped when inside labelledby traversal)
70
+ if (!inLabelledbyTraversal) {
71
+ const labelledbyResult = resolveAriaLabelledby(el, resolver, visited, computeAccessibleNameInternal);
72
+ if (labelledbyResult) {
73
+ return labelledbyResult;
74
+ }
75
+ }
76
+ // AccName 1.2 §4.3.2 Step 2D: aria-label
77
+ const ariaLabelResult = resolveAriaLabel(el);
78
+ if (ariaLabelResult) {
79
+ return ariaLabelResult;
80
+ }
81
+ // AccName 1.2 §4.3.2 Step 2E: Element-specific name (HTML-AAM §4.1)
82
+ const elementResult = getElementSpecificName(el, resolver, visited, computeAccessibleNameInternal, inLabelledbyTraversal);
83
+ if (elementResult) {
84
+ return elementResult;
85
+ }
86
+ // AccName 1.2 §4.3.2 Step 2F/2C: Name from content
87
+ // Applies when the role allows nameFrom: ["content"] OR when the element is
88
+ // directly referenced by aria-labelledby (inLabelledbyTraversal).
89
+ if (resolver.allowsNameFromContent(el) || inLabelledbyTraversal) {
90
+ const content = resolveNameFromContent(el, resolver, visited, computeAccessibleNameInternal, inLabelledbyTraversal);
91
+ if (content.trim()) {
92
+ return makeResult(content, 'content');
93
+ }
94
+ }
95
+ // AccName 1.2 §4.3.2 Step 2I: Title attribute fallback
96
+ const title = el.getAttribute('title');
97
+ if (title?.trim()) {
98
+ return makeResult(title, 'title');
99
+ }
100
+ return EMPTY_RESULT;
101
+ }
@@ -0,0 +1,36 @@
1
+ import type { AccnameElement, AccnameResolver, AccnameResult } from './types.js';
2
+ /**
3
+ * Computes element-specific accessible name per HTML-AAM §4.1.
4
+ *
5
+ * Implements AccName 1.2 §4.3.2 Step 2E: for elements that have a native
6
+ * host language text alternative, use that alternative. The specific rules
7
+ * for each HTML element are defined in HTML-AAM §4.1.
8
+ *
9
+ * Returns null if no element-specific rule applies, letting the caller
10
+ * fall through to name-from-content (Step 2F) or title fallback (Step 2I).
11
+ *
12
+ * Control flow (dispatches by `localName`):
13
+ * - SVG elements → `handleSvgElement` (SVG-AAM: `<title>` child)
14
+ * - `<input>` → `handleInput` (branches by type: text-like, button, image, hidden)
15
+ * - `<textarea>`, `<select>`, `<meter>`, `<progress>`, `<output>` → `handleLabelableWithTitle`
16
+ * - `<button>` → `handleButton` (label → content → title)
17
+ * - `<fieldset>` → `handleFieldset` (legend → title)
18
+ * - `<table>` → `handleTable` (caption → title)
19
+ * - `<img>` → `handleImg` (alt → title)
20
+ * - `<area>` → `handleArea` (alt → title)
21
+ * - `<figure>` → `handleFigure` (title only)
22
+ * - `<summary>` → `handleSummary` (content → title)
23
+ * - `<a href>` → `handleAnchor` (content → title)
24
+ * - `<iframe>` → `handleTitleOnly` (title only)
25
+ * - All others → null (rely on caller's generic Steps 2F/2I)
26
+ *
27
+ * @param el - The element to compute the name for
28
+ * @param resolver - Environment-dependent resolver for DOM traversal and role queries
29
+ * @param visited - Set of element IDs already visited (cycle prevention)
30
+ * @param computeFn - The recursive accessible name computation function
31
+ * @param inLabelledbyTraversal - Whether this computation is part of an aria-labelledby traversal
32
+ * @returns The computed name result, or null if no element-specific rule applies
33
+ * @see https://www.w3.org/TR/accname-1.2/#computation-steps — AccName 1.2 §4.3.2 Step 2E
34
+ * @see https://www.w3.org/TR/html-aam-1.0/#accessible-name-and-description-computation — HTML-AAM §4.1
35
+ */
36
+ export declare function getElementSpecificName(el: AccnameElement, resolver: AccnameResolver, visited: ReadonlySet<string>, computeFn: (el: AccnameElement, resolver: AccnameResolver, inLabelledbyTraversal: boolean, visited: ReadonlySet<string>) => AccnameResult, inLabelledbyTraversal: boolean): AccnameResult | null;