@surea11y/core 1.0.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 (164) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +145 -0
  4. package/bin/core.js +244 -0
  5. package/docs/BINDING_AUTHORS_GUIDE.md +41 -0
  6. package/docs/CLI.md +49 -0
  7. package/docs/ENGINE_OPTIONS.md +155 -0
  8. package/docs/I18N.md +47 -0
  9. package/docs/INTEGRATION.md +156 -0
  10. package/docs/LIMITATIONS.md +31 -0
  11. package/docs/OUTPUT_SCHEMA.md +237 -0
  12. package/docs/POLICY.md +71 -0
  13. package/docs/RULE_AUTHORING.md +375 -0
  14. package/docs/RULE_CATALOG.md +180 -0
  15. package/docs/RULE_TAXONOMY.md +145 -0
  16. package/docs/TROUBLESHOOTING.md +48 -0
  17. package/docs/WCAG_CONFORMANCE.md +49 -0
  18. package/package.json +60 -0
  19. package/src/catalogs/composites.wcag.js +490 -0
  20. package/src/checks/automatic/area-alt-present.js +225 -0
  21. package/src/checks/automatic/aria-allowed-attr.js +206 -0
  22. package/src/checks/automatic/aria-allowed-role.js +102 -0
  23. package/src/checks/automatic/aria-braille-equivalent.js +139 -0
  24. package/src/checks/automatic/aria-conditional-attr.js +110 -0
  25. package/src/checks/automatic/aria-deprecated-role.js +106 -0
  26. package/src/checks/automatic/aria-hidden-body.js +87 -0
  27. package/src/checks/automatic/aria-hidden-focus.js +480 -0
  28. package/src/checks/automatic/aria-prohibited-attr.js +156 -0
  29. package/src/checks/automatic/aria-prohibited-children.js +265 -0
  30. package/src/checks/automatic/aria-required-attr.js +154 -0
  31. package/src/checks/automatic/aria-required-children.js +274 -0
  32. package/src/checks/automatic/aria-required-parent.js +222 -0
  33. package/src/checks/automatic/aria-role-name-present.js +201 -0
  34. package/src/checks/automatic/aria-roles-valid.js +110 -0
  35. package/src/checks/automatic/aria-valid-attr-value.js +123 -0
  36. package/src/checks/automatic/aria-valid-attr.js +109 -0
  37. package/src/checks/automatic/autocomplete-valid.js +134 -0
  38. package/src/checks/automatic/avoid-inline-spacing.js +107 -0
  39. package/src/checks/automatic/binary-control-name-present.js +294 -0
  40. package/src/checks/automatic/button-name-present.js +146 -0
  41. package/src/checks/automatic/bypass-blocks-present.js +162 -0
  42. package/src/checks/automatic/canvas-text-alternative-present.js +140 -0
  43. package/src/checks/automatic/combobox-name-present.js +267 -0
  44. package/src/checks/automatic/contrast-computable.js +378 -0
  45. package/src/checks/automatic/contrast-enhanced.js +517 -0
  46. package/src/checks/automatic/contrast-minimum.js +512 -0
  47. package/src/checks/automatic/css-orientation-lock.js +206 -0
  48. package/src/checks/automatic/definition-list-children-valid.js +148 -0
  49. package/src/checks/automatic/deprecated-elements-not-used.js +91 -0
  50. package/src/checks/automatic/dialog-name-present.js +209 -0
  51. package/src/checks/automatic/dlitem-parent-valid.js +100 -0
  52. package/src/checks/automatic/duplicate-id-aria.js +126 -0
  53. package/src/checks/automatic/embed-text-alternative-present.js +190 -0
  54. package/src/checks/automatic/form-control-programmatic-label-present.js +409 -0
  55. package/src/checks/automatic/form-control-single-label.js +117 -0
  56. package/src/checks/automatic/html-xml-lang-mismatch.js +91 -0
  57. package/src/checks/automatic/iframe-focusable-content.js +141 -0
  58. package/src/checks/automatic/iframe-name-present.js +102 -0
  59. package/src/checks/automatic/iframe-title-unique.js +107 -0
  60. package/src/checks/automatic/img-alt-present.js +223 -0
  61. package/src/checks/automatic/input-image-alt-present.js +155 -0
  62. package/src/checks/automatic/label-in-name.js +326 -0
  63. package/src/checks/automatic/language-page-present.js +159 -0
  64. package/src/checks/automatic/link-in-text-block.js +218 -0
  65. package/src/checks/automatic/link-name-present.js +114 -0
  66. package/src/checks/automatic/list-children-valid.js +152 -0
  67. package/src/checks/automatic/listbox-name-present.js +236 -0
  68. package/src/checks/automatic/listitem-parent-valid.js +118 -0
  69. package/src/checks/automatic/menuitem-name-present.js +201 -0
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +105 -0
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +107 -0
  72. package/src/checks/automatic/meta-viewport-zoom-enabled.js +118 -0
  73. package/src/checks/automatic/meter-name-present.js +160 -0
  74. package/src/checks/automatic/nested-interactive-controls-absent.js +135 -0
  75. package/src/checks/automatic/object-text-alternative-present.js +193 -0
  76. package/src/checks/automatic/option-name-present.js +157 -0
  77. package/src/checks/automatic/page-title-present.js +86 -0
  78. package/src/checks/automatic/progressbar-name-present.js +165 -0
  79. package/src/checks/automatic/role-img-alt-present.js +206 -0
  80. package/src/checks/automatic/searchbox-name-present.js +236 -0
  81. package/src/checks/automatic/server-side-image-map-absent.js +88 -0
  82. package/src/checks/automatic/slider-name-present.js +276 -0
  83. package/src/checks/automatic/spinbutton-name-present.js +236 -0
  84. package/src/checks/automatic/summary-name-present.js +153 -0
  85. package/src/checks/automatic/svg-image-text-alternative-present.js +220 -0
  86. package/src/checks/automatic/svg-text-alternative-present.js +298 -0
  87. package/src/checks/automatic/tab-name-present.js +200 -0
  88. package/src/checks/automatic/table-headers-attr-valid.js +122 -0
  89. package/src/checks/automatic/table-th-has-data-cells.js +117 -0
  90. package/src/checks/automatic/target-size-minimum.js +605 -0
  91. package/src/checks/automatic/td-has-header.js +151 -0
  92. package/src/checks/automatic/textbox-name-present.js +236 -0
  93. package/src/checks/automatic/tooltip-name-present.js +158 -0
  94. package/src/checks/automatic/treeitem-name-present.js +157 -0
  95. package/src/checks/automatic/valid-lang.js +100 -0
  96. package/src/checks/automatic/video-poster-text-alternative-present.js +193 -0
  97. package/src/checks/manual/accesskeys-manual.js +93 -0
  98. package/src/checks/manual/area-alt-decorative-manual.js +247 -0
  99. package/src/checks/manual/area-alt-quality-manual.js +204 -0
  100. package/src/checks/manual/aria-checked-state-mismatch-manual.js +141 -0
  101. package/src/checks/manual/aria-text-manual.js +109 -0
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +170 -0
  103. package/src/checks/manual/css-hidden-focus.js +259 -0
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +204 -0
  105. package/src/checks/manual/empty-heading-manual.js +182 -0
  106. package/src/checks/manual/empty-table-header-manual.js +163 -0
  107. package/src/checks/manual/focus-order-semantics-manual.js +117 -0
  108. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +291 -0
  109. package/src/checks/manual/heading-order-manual.js +130 -0
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +142 -0
  111. package/src/checks/manual/image-redundant-alt-manual.js +118 -0
  112. package/src/checks/manual/img-alt-decorative-manual.js +148 -0
  113. package/src/checks/manual/img-alt-quality-manual.js +182 -0
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +144 -0
  115. package/src/checks/manual/input-image-alt-quality-manual.js +144 -0
  116. package/src/checks/manual/label-title-only-manual.js +115 -0
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +180 -0
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +169 -0
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +167 -0
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +177 -0
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +169 -0
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +132 -0
  123. package/src/checks/manual/landmark-one-main-manual.js +151 -0
  124. package/src/checks/manual/landmark-unique-manual.js +252 -0
  125. package/src/checks/manual/link-name-quality-manual.js +143 -0
  126. package/src/checks/manual/media-transcript-present-manual.js +373 -0
  127. package/src/checks/manual/meta-viewport-large-manual.js +119 -0
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +134 -0
  129. package/src/checks/manual/no-autoplay-audio-manual.js +116 -0
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +194 -0
  131. package/src/checks/manual/p-as-heading-manual.js +163 -0
  132. package/src/checks/manual/page-has-heading-one-manual.js +110 -0
  133. package/src/checks/manual/page-title-patterns-manual.js +262 -0
  134. package/src/checks/manual/presentation-role-conflict-manual.js +159 -0
  135. package/src/checks/manual/region-manual.js +183 -0
  136. package/src/checks/manual/scope-attr-valid-manual.js +93 -0
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +168 -0
  138. package/src/checks/manual/skip-link-manual.js +150 -0
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +209 -0
  140. package/src/checks/manual/tabindex-manual.js +94 -0
  141. package/src/checks/manual/table-duplicate-name-manual.js +99 -0
  142. package/src/checks/manual/table-fake-caption-manual.js +122 -0
  143. package/src/checks/manual/video-caption-manual.js +118 -0
  144. package/src/checks/manual-review.js +95 -0
  145. package/src/checks/rules-and-tags.full.csv +19 -0
  146. package/src/checks/rules-and-tags.full.json +259 -0
  147. package/src/core/aria-helpers.js +906 -0
  148. package/src/core/contrast-helpers.js +1147 -0
  149. package/src/core/dom-helpers.js +4085 -0
  150. package/src/core/dom-runner.js +627 -0
  151. package/src/core/frame-messaging.js +210 -0
  152. package/src/core/frame-scan.js +178 -0
  153. package/src/core/rollup-composites.js +135 -0
  154. package/src/core/rule-meta.js +140 -0
  155. package/src/core.js +79055 -0
  156. package/src/coverage/wcag-facets.js +1079 -0
  157. package/src/coverage/wcag-version-map.js +84 -0
  158. package/src/i18n/en.js +919 -0
  159. package/src/i18n/fr.js +527 -0
  160. package/src/index.js +4 -0
  161. package/src/policy/contracts.js +18 -0
  162. package/src/policy/resolvePolicy.js +55 -0
  163. package/src/policy/schemas/engine-options.schema.json +103 -0
  164. package/src/policy/schemas/policy-contract.schema.json +40 -0
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check dlitem-parent-valid
5
+ * @atomic true
6
+ * @summary <dt>/<dd> elements must be contained by a <dl> (directly or via one wrapping <div>)
7
+ * @standard WCAG 2.2
8
+ * @sc 1.3.1
9
+ * @applicability
10
+ * Applies to <dt>/<dd> elements that have a parent element.
11
+ * @expectation
12
+ * The parent is <dl>, or the parent is a <div> whose own parent is <dl>
13
+ * (a single level of wrapping div is allowed, matching how authors
14
+ * commonly group dt/dd pairs). A <dt>/<dd> used outside a real
15
+ * description-list container is not exposed as a term/definition to
16
+ * assistive technologies.
17
+ * @implementation-notes
18
+ * - Distinct, atomic decision from definition-list-children-valid
19
+ * (the inverse relationship: does a given <dl> have valid children).
20
+ */
21
+
22
+ const id = 'dlitem-parent-valid';
23
+
24
+ const meta = {
25
+ title: 'Description-list items must be inside a description list',
26
+ description: 'Checks that <dt>/<dd> elements are contained by a <dl>, directly or via one wrapping <div>.',
27
+ i18n: {
28
+ titleKey: 'dlitemParentValid_title',
29
+ descriptionKey: 'dlitemParentValid_description'
30
+ },
31
+ helpUrl: null,
32
+ tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
33
+ wcagSc: ['1.3.1'],
34
+ normativeMappings: [
35
+ { standard: 'WCAG', version: '2.2', requirement: '1.3.1', title: 'Info and Relationships', conformanceLevel: 'A' }
36
+ ],
37
+ defaultSeverity: 'serious',
38
+ category: 'perceivable',
39
+ type: 'automatic',
40
+ defaultConfidence: 'high',
41
+ coverage: { facetsBySc: { '1.3.1': ['dlitem-parent-valid'] } }
42
+ };
43
+
44
+ function runInPage(ctx) {
45
+ const { document, root, helpers, rule } = ctx;
46
+ const safeRoot = root || document;
47
+
48
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dt, dd', safeRoot) : helpers.queryAll('dt, dd', safeRoot);
49
+
50
+ const occurrences = [];
51
+ let applicableCount = 0;
52
+
53
+ for (const el of nodes) {
54
+ if (!el) continue;
55
+ const parent = el.parentElement;
56
+ if (!parent) continue;
57
+
58
+ applicableCount += 1;
59
+
60
+ const parentTag = parent.tagName ? parent.tagName.toLowerCase() : '';
61
+ let valid = parentTag === 'dl';
62
+
63
+ if (!valid && parentTag === 'div') {
64
+ const grandparent = parent.parentElement;
65
+ const grandparentTag = grandparent && grandparent.tagName ? grandparent.tagName.toLowerCase() : '';
66
+ valid = grandparentTag === 'dl';
67
+ }
68
+
69
+ if (valid) continue;
70
+
71
+ const tag = el.tagName.toLowerCase();
72
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
73
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
74
+
75
+ occurrences.push({
76
+ selector: stableSelector,
77
+ html,
78
+ summary: 'This description-list item is not contained by a <dl>.',
79
+ hint: 'Place this <dt>/<dd> inside a <dl>, directly or wrapped in a single <div>.',
80
+ i18n: {
81
+ summaryKey: 'dlitemParentValid_summary_fail',
82
+ hintKey: 'dlitemParentValid_hint_fail',
83
+ params: { element: tag, parentElement: parentTag }
84
+ },
85
+ data: {
86
+ details: { reasonCode: 'DLITEM_INVALID_PARENT', element: tag, parentElement: parentTag }
87
+ }
88
+ });
89
+ }
90
+
91
+ if (applicableCount === 0) {
92
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
93
+ }
94
+ if (occurrences.length) {
95
+ return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
96
+ }
97
+ return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
98
+ }
99
+
100
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check duplicate-id-aria
5
+ * @atomic true
6
+ * @summary Any id referenced by an ARIA ID-reference attribute must be unique in the document
7
+ * @standard WCAG 2.2
8
+ * @sc 4.1.2
9
+ * @applicability
10
+ * Applies when the document contains at least one non-empty
11
+ * aria-labelledby, aria-describedby, aria-owns, aria-controls,
12
+ * aria-activedescendant, aria-flowto, aria-errormessage, or aria-details
13
+ * attribute (i.e. at least one ARIA ID reference exists to resolve).
14
+ * @expectation
15
+ * For every id value referenced by one of those attributes, exactly one
16
+ * element in the document carries that id. A duplicated id referenced by
17
+ * ARIA is ambiguous: assistive technologies cannot reliably determine
18
+ * which element the reference resolves to (typically the first, silently
19
+ * dropping the others).
20
+ * @implementation-notes
21
+ * - Scoped deliberately to ids referenced by ARIA (matching a widely-used
22
+ * reference engine's duplicate-id-aria, not the broader/deprecated page-wide duplicate-id
23
+ * check — see ROADMAP.md's "Skip" list).
24
+ * - Document-wide by design: id uniqueness and ARIA id references are a
25
+ * whole-document property, not scoped to a sub-root.
26
+ */
27
+
28
+ const id = 'duplicate-id-aria';
29
+
30
+ const meta = {
31
+ title: 'IDs referenced by ARIA must be unique',
32
+ description: 'Checks that any id value referenced by an ARIA ID-reference attribute (aria-labelledby, aria-describedby, aria-owns, aria-controls, aria-activedescendant, aria-flowto, aria-errormessage, aria-details) is unique in the document.',
33
+ i18n: {
34
+ titleKey: 'duplicateIdAria_title',
35
+ descriptionKey: 'duplicateIdAria_description'
36
+ },
37
+ helpUrl: null,
38
+ tags: ['wcag2a', 'wcag412', 'aria', 'structure', 'atomic', 'automatic'],
39
+ wcagSc: ['4.1.2'],
40
+ normativeMappings: [
41
+ { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
42
+ ],
43
+ defaultSeverity: 'serious',
44
+ category: 'robust',
45
+ type: 'automatic',
46
+ defaultConfidence: 'high',
47
+ coverage: { facetsBySc: { '4.1.2': ['duplicate-id-aria'] } }
48
+ };
49
+
50
+ function runInPage(ctx) {
51
+ const { document, helpers, rule } = ctx;
52
+
53
+ // Declared inside runInPage — see scripts/build-core.js header
54
+ // ("runInPage MUST be self-contained").
55
+ const IDREF_ATTRS = [
56
+ 'aria-labelledby',
57
+ 'aria-describedby',
58
+ 'aria-owns',
59
+ 'aria-controls',
60
+ 'aria-activedescendant',
61
+ 'aria-flowto',
62
+ 'aria-errormessage',
63
+ 'aria-details'
64
+ ];
65
+
66
+ const idMap = new Map(); // id -> element[]
67
+ const idNodes = document.querySelectorAll ? document.querySelectorAll('[id]') : [];
68
+ for (const el of idNodes) {
69
+ if (!el || !el.getAttribute) continue;
70
+ const value = String(el.getAttribute('id') || '').trim();
71
+ if (!value) continue;
72
+ if (!idMap.has(value)) idMap.set(value, []);
73
+ idMap.get(value).push(el);
74
+ }
75
+
76
+ const referencedIds = new Set();
77
+ for (const attr of IDREF_ATTRS) {
78
+ const selector = `[${attr}]`;
79
+ const nodes = document.querySelectorAll ? document.querySelectorAll(selector) : [];
80
+ for (const el of nodes) {
81
+ if (!el || !el.getAttribute) continue;
82
+ const raw = String(el.getAttribute(attr) || '').trim();
83
+ if (!raw) continue;
84
+ for (const token of raw.split(/\s+/)) {
85
+ if (token) referencedIds.add(token);
86
+ }
87
+ }
88
+ }
89
+
90
+ const occurrences = [];
91
+
92
+ for (const refId of referencedIds) {
93
+ const els = idMap.get(refId) || [];
94
+ if (els.length <= 1) continue;
95
+
96
+ for (const el of els) {
97
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
98
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
99
+
100
+ occurrences.push({
101
+ selector: stableSelector,
102
+ html,
103
+ summary: 'This id is referenced by an ARIA attribute but is used by more than one element.',
104
+ hint: 'Make ids referenced by ARIA attributes unique within the document.',
105
+ i18n: {
106
+ summaryKey: 'duplicateIdAria_summary_fail',
107
+ hintKey: 'duplicateIdAria_hint_fail',
108
+ params: { id: refId, duplicateCount: String(els.length) }
109
+ },
110
+ data: {
111
+ details: { reasonCode: 'DUPLICATE_ID_ARIA_REFERENCED', id: refId, duplicateCount: els.length }
112
+ }
113
+ });
114
+ }
115
+ }
116
+
117
+ if (referencedIds.size === 0) {
118
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
119
+ }
120
+ if (occurrences.length) {
121
+ return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
122
+ }
123
+ return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
124
+ }
125
+
126
+ module.exports = { id, meta, runInPage };
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @check embed-text-alternative-present
5
+ * @atomic true
6
+ * @summary Accessible <embed> elements must provide a text alternative
7
+ * @standard WCAG 2.2
8
+ * @sc 1.1.1
9
+ * @applicability
10
+ * Applies to <embed> elements that are exposed to assistive technologies.
11
+ * Elements otherwise hidden from the accessibility tree remain applicable
12
+ * if they are tabbable or referenced by IDREF relationships (per engine eligibility checks).
13
+ * role="presentation"/role="none" are excluded only when not focusable.
14
+ * @expectation
15
+ * Each applicable <embed> provides a text alternative via:
16
+ * - an accessible name (aria-labelledby/aria-label), OR
17
+ * - a title attribute (best-effort fallback).
18
+ *
19
+ * Note: <embed> does not support fallback content in HTML, so this rule does not check children.
20
+ */
21
+
22
+ const id = 'embed-text-alternative-present';
23
+
24
+ const meta = {
25
+ title: '<embed> must provide a text alternative',
26
+ description: 'Checks that <embed> elements provide a text alternative via an accessible name.',
27
+ i18n: {
28
+ titleKey: 'embed_textAltPresent_title',
29
+ descriptionKey: 'embed_textAltPresent_description'
30
+ },
31
+ helpUrl: null,
32
+ tags: ['wcag2a', 'wcag111', 'nontext', 'embed', 'atomic', 'automatic'],
33
+ wcagSc: ['1.1.1'],
34
+ normativeMappings: [
35
+ { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
36
+ ],
37
+ defaultSeverity: 'serious',
38
+ category: 'perceivable',
39
+ type: 'automatic',
40
+ defaultConfidence: 'high',
41
+ coverage: { facetsBySc: { '1.1.1': ['embed-text-alternative-present'] } }
42
+ };
43
+
44
+ function runInPage(ctx) {
45
+ const { document, root, helpers, rule } = ctx;
46
+ const safeRoot = root || document;
47
+
48
+ const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
49
+ const queryAll = helpers && typeof helpers.queryAll === 'function'
50
+ ? helpers.queryAll
51
+ : (sel) => {
52
+ try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
53
+ catch { return []; }
54
+ };
55
+
56
+ const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
57
+ ? helpers.getEligibilityInfo
58
+ : null;
59
+
60
+ const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
61
+ ? helpers.isAccTreeEligible
62
+ : null;
63
+
64
+ const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
65
+ ? helpers.getFocusableInfo
66
+ : null;
67
+
68
+ const getAriaNameInfo = helpers && typeof helpers.getAriaNameInfo === 'function'
69
+ ? helpers.getAriaNameInfo
70
+ : null;
71
+
72
+ const embeds = (() => {
73
+ try { return Array.from((queryAllSmart ? queryAllSmart('embed') : queryAll('embed')) || []); }
74
+ catch { return queryAll('embed'); }
75
+ })();
76
+
77
+ if (!embeds.length) {
78
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
79
+ }
80
+
81
+ const occurrences = [];
82
+ let applicableCount = 0;
83
+
84
+ const trim = (v) => (v == null ? '' : String(v)).trim();
85
+
86
+ function computeNameInfo(el) {
87
+ // For <embed>, treat ARIA name as primary. Do not accept HTML <label> associations.
88
+ const flags = [];
89
+ let aria = null;
90
+
91
+ if (getAriaNameInfo) {
92
+ aria = (() => { try { return getAriaNameInfo(el, ctx); } catch { return null; } })();
93
+ }
94
+
95
+ if (aria && aria.present && trim(aria.value)) {
96
+ return { present: true, value: trim(aria.value), mechanism: aria.mechanism || 'aria', flags: (aria.flags || []).slice(0) };
97
+ }
98
+
99
+ const title = trim(el.getAttribute && el.getAttribute('title'));
100
+ if (title) {
101
+ flags.push('title-used');
102
+ return { present: true, value: title, mechanism: 'title', flags };
103
+ }
104
+
105
+ if (aria && aria.flags && aria.flags.length) {
106
+ for (const f of aria.flags) flags.push(f);
107
+ }
108
+
109
+ return { present: false, value: '', mechanism: 'none', flags };
110
+ }
111
+
112
+ // `title` is a weaker text-alternative mechanism than aria-label/
113
+ // aria-labelledby — it is not reliably exposed to assistive technology
114
+ // in every context (e.g. touch/mobile) — so a pass achieved only via
115
+ // `title` is reported at reduced confidence rather than the rule's
116
+ // default `high`.
117
+ let anyPassedViaWeakMechanism = false;
118
+
119
+ for (const el of embeds) {
120
+ if (!el || !el.getAttribute) continue;
121
+
122
+ if (isAccTreeEligible) {
123
+ const elig = (() => { try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; } })();
124
+ if (elig && elig.eligible === false) continue;
125
+ }
126
+
127
+ // role presentation/none exclusion only when not focusable
128
+ const role = (() => { try { return String(el.getAttribute('role') || '').trim().toLowerCase(); } catch { return ''; } })();
129
+ if (role === 'presentation' || role === 'none') {
130
+ let focusable = false;
131
+ if (getFocusableInfo) {
132
+ const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
133
+ focusable = !!(fi && fi.focusable);
134
+ } else {
135
+ const tabindex = el.getAttribute('tabindex');
136
+ focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
137
+ }
138
+ if (!focusable) continue;
139
+ }
140
+
141
+ applicableCount += 1;
142
+
143
+ const name = computeNameInfo(el);
144
+ if (name.present) {
145
+ if (name.mechanism === 'title') anyPassedViaWeakMechanism = true;
146
+ continue;
147
+ }
148
+
149
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
150
+
151
+ const baseOccurrence = {
152
+ summary: 'Missing text alternative for <embed>.',
153
+ hint: 'Add an accessible name to <embed> (aria-label/aria-labelledby).',
154
+ i18n: {
155
+ summaryKey: 'embed_textAltPresent_summary_fail',
156
+ hintKey: 'embed_textAltPresent_hint_fail',
157
+ params: { element: 'embed' }
158
+ },
159
+ data: {
160
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
161
+ name
162
+ }
163
+ };
164
+
165
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
166
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
167
+ } else {
168
+ // Never compute selector/snippet in the rule.
169
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
170
+ }
171
+ }
172
+
173
+ if (applicableCount === 0) {
174
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
175
+ }
176
+
177
+ if (!occurrences.length) {
178
+ return {
179
+ ruleId: rule.ruleId,
180
+ outcome: 'pass',
181
+ severity: 'minor',
182
+ confidence: anyPassedViaWeakMechanism ? 'medium' : 'high',
183
+ occurrences: []
184
+ };
185
+ }
186
+
187
+ return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
188
+ }
189
+
190
+ module.exports = { id, meta, runInPage };