@surea11y/core 1.2.0 → 1.4.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 (168) hide show
  1. package/CHANGELOG.md +81 -7
  2. package/LICENSE +373 -21
  3. package/README.md +175 -35
  4. package/bin/surea11y-core.js +20 -0
  5. package/docs/API_STABILITY.md +27 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/ENGINE_OPTIONS.md +2 -0
  9. package/docs/I18N.md +12 -9
  10. package/docs/INTEGRATION.md +19 -1
  11. package/docs/LIMITATIONS.md +1 -1
  12. package/docs/OUTPUT_SCHEMA.md +1 -1
  13. package/docs/REPORT.md +1 -1
  14. package/docs/RULE_CATALOG.md +1 -1
  15. package/docs/SARIF.md +59 -0
  16. package/package.json +63 -18
  17. package/src/baseline.js +0 -0
  18. package/src/checks/automatic/area-alt-present.js +63 -31
  19. package/src/checks/automatic/aria-allowed-attr.js +204 -80
  20. package/src/checks/automatic/aria-allowed-role.js +23 -7
  21. package/src/checks/automatic/aria-braille-equivalent.js +34 -10
  22. package/src/checks/automatic/aria-conditional-attr.js +32 -14
  23. package/src/checks/automatic/aria-deprecated-role.js +26 -11
  24. package/src/checks/automatic/aria-hidden-body.js +48 -23
  25. package/src/checks/automatic/aria-hidden-focus.js +420 -66
  26. package/src/checks/automatic/aria-prohibited-attr.js +327 -60
  27. package/src/checks/automatic/aria-prohibited-children.js +111 -103
  28. package/src/checks/automatic/aria-required-attr.js +29 -15
  29. package/src/checks/automatic/aria-required-children.js +44 -24
  30. package/src/checks/automatic/aria-required-parent.js +64 -35
  31. package/src/checks/automatic/aria-role-name-present.js +49 -21
  32. package/src/checks/automatic/aria-roles-valid.js +24 -12
  33. package/src/checks/automatic/aria-valid-attr-value.js +46 -22
  34. package/src/checks/automatic/aria-valid-attr.js +19 -5
  35. package/src/checks/automatic/autocomplete-valid.js +76 -16
  36. package/src/checks/automatic/avoid-inline-spacing.js +23 -8
  37. package/src/checks/automatic/binary-control-name-present.js +62 -50
  38. package/src/checks/automatic/button-name-present.js +54 -24
  39. package/src/checks/automatic/bypass-blocks-present.js +51 -32
  40. package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
  41. package/src/checks/automatic/combobox-name-present.js +40 -45
  42. package/src/checks/automatic/contrast-computable.js +363 -341
  43. package/src/checks/automatic/contrast-enhanced.js +489 -466
  44. package/src/checks/automatic/contrast-minimum.js +488 -465
  45. package/src/checks/automatic/css-orientation-lock.js +51 -35
  46. package/src/checks/automatic/definition-list-children-valid.js +46 -25
  47. package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
  48. package/src/checks/automatic/dialog-name-present.js +47 -85
  49. package/src/checks/automatic/dlitem-parent-valid.js +25 -8
  50. package/src/checks/automatic/duplicate-id-aria.js +28 -9
  51. package/src/checks/automatic/embed-text-alternative-present.js +88 -35
  52. package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
  53. package/src/checks/automatic/form-control-single-label.js +50 -14
  54. package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
  55. package/src/checks/automatic/iframe-focusable-content.js +265 -22
  56. package/src/checks/automatic/iframe-name-present.js +33 -9
  57. package/src/checks/automatic/iframe-title-unique.js +32 -9
  58. package/src/checks/automatic/img-alt-present.js +54 -52
  59. package/src/checks/automatic/input-image-alt-present.js +141 -112
  60. package/src/checks/automatic/label-in-name.js +65 -41
  61. package/src/checks/automatic/language-page-present.js +111 -109
  62. package/src/checks/automatic/link-in-text-block.js +61 -19
  63. package/src/checks/automatic/link-name-present.js +47 -14
  64. package/src/checks/automatic/list-children-valid.js +40 -33
  65. package/src/checks/automatic/listbox-name-present.js +41 -19
  66. package/src/checks/automatic/listitem-parent-valid.js +48 -13
  67. package/src/checks/automatic/menuitem-name-present.js +41 -61
  68. package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
  69. package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
  70. package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
  71. package/src/checks/automatic/meter-name-present.js +40 -36
  72. package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
  73. package/src/checks/automatic/object-text-alternative-present.js +93 -39
  74. package/src/checks/automatic/option-name-present.js +40 -21
  75. package/src/checks/automatic/page-title-present.js +19 -6
  76. package/src/checks/automatic/progressbar-name-present.js +49 -44
  77. package/src/checks/automatic/role-img-alt-present.js +211 -159
  78. package/src/checks/automatic/searchbox-name-present.js +41 -19
  79. package/src/checks/automatic/server-side-image-map-absent.js +27 -11
  80. package/src/checks/automatic/slider-name-present.js +42 -47
  81. package/src/checks/automatic/spinbutton-name-present.js +41 -19
  82. package/src/checks/automatic/summary-name-present.js +39 -17
  83. package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
  84. package/src/checks/automatic/svg-text-alternative-present.js +262 -230
  85. package/src/checks/automatic/tab-name-present.js +39 -60
  86. package/src/checks/automatic/table-headers-attr-valid.js +27 -10
  87. package/src/checks/automatic/table-th-has-data-cells.js +24 -8
  88. package/src/checks/automatic/target-size-minimum.js +123 -48
  89. package/src/checks/automatic/td-has-header.js +53 -12
  90. package/src/checks/automatic/textbox-name-present.js +41 -19
  91. package/src/checks/automatic/tooltip-name-present.js +39 -18
  92. package/src/checks/automatic/treeitem-name-present.js +40 -21
  93. package/src/checks/automatic/valid-lang.js +22 -6
  94. package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
  95. package/src/checks/manual/accesskeys-manual.js +17 -6
  96. package/src/checks/manual/area-alt-decorative-manual.js +194 -193
  97. package/src/checks/manual/area-alt-quality-manual.js +184 -141
  98. package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
  99. package/src/checks/manual/aria-text-manual.js +20 -11
  100. package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
  101. package/src/checks/manual/css-hidden-focus.js +375 -169
  102. package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
  103. package/src/checks/manual/empty-heading-manual.js +41 -24
  104. package/src/checks/manual/empty-table-header-manual.js +69 -31
  105. package/src/checks/manual/focus-order-semantics-manual.js +60 -13
  106. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
  107. package/src/checks/manual/heading-order-manual.js +50 -8
  108. package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
  109. package/src/checks/manual/image-redundant-alt-manual.js +38 -8
  110. package/src/checks/manual/img-alt-decorative-manual.js +133 -96
  111. package/src/checks/manual/img-alt-quality-manual.js +178 -127
  112. package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
  113. package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
  114. package/src/checks/manual/label-title-only-manual.js +44 -28
  115. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
  116. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
  117. package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
  118. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
  119. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
  120. package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
  121. package/src/checks/manual/landmark-one-main-manual.js +38 -43
  122. package/src/checks/manual/landmark-unique-manual.js +78 -67
  123. package/src/checks/manual/link-name-quality-manual.js +45 -12
  124. package/src/checks/manual/media-transcript-present-manual.js +37 -22
  125. package/src/checks/manual/meta-viewport-large-manual.js +19 -6
  126. package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
  127. package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
  128. package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
  129. package/src/checks/manual/p-as-heading-manual.js +24 -7
  130. package/src/checks/manual/page-has-heading-one-manual.js +42 -32
  131. package/src/checks/manual/page-title-patterns-manual.js +80 -50
  132. package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
  133. package/src/checks/manual/region-manual.js +244 -60
  134. package/src/checks/manual/scope-attr-valid-manual.js +13 -4
  135. package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
  136. package/src/checks/manual/skip-link-manual.js +42 -18
  137. package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
  138. package/src/checks/manual/tabindex-manual.js +13 -4
  139. package/src/checks/manual/table-duplicate-name-manual.js +22 -11
  140. package/src/checks/manual/table-fake-caption-manual.js +48 -10
  141. package/src/checks/manual/video-caption-manual.js +17 -4
  142. package/src/checks/manual-review.js +58 -12
  143. package/src/core.js +41705 -29650
  144. package/src/index.js +2 -0
  145. package/src/report.js +109 -47
  146. package/src/sarif.js +190 -0
  147. package/surea11y.browser.js +37774 -0
  148. package/bin/core.js +0 -348
  149. package/docs/CLI.md +0 -75
  150. package/src/catalogs/composites.wcag.js +0 -490
  151. package/src/checks/rules-and-tags.full.csv +0 -19
  152. package/src/checks/rules-and-tags.full.json +0 -259
  153. package/src/core/aria-helpers.js +0 -970
  154. package/src/core/contrast-helpers.js +0 -1147
  155. package/src/core/dom-helpers.js +0 -4235
  156. package/src/core/dom-runner.js +0 -671
  157. package/src/core/frame-messaging.js +0 -210
  158. package/src/core/frame-scan.js +0 -178
  159. package/src/core/rollup-composites.js +0 -135
  160. package/src/core/rule-meta.js +0 -159
  161. package/src/coverage/wcag-facets.js +0 -1079
  162. package/src/coverage/wcag-version-map.js +0 -84
  163. package/src/i18n/en.js +0 -923
  164. package/src/i18n/fr.js +0 -844
  165. package/src/policy/contracts.js +0 -18
  166. package/src/policy/resolvePolicy.js +0 -55
  167. package/src/policy/schemas/engine-options.schema.json +0 -103
  168. package/src/policy/schemas/policy-contract.schema.json +0 -40
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -22,7 +24,8 @@ const id = 'svg-image-text-alternative-present';
22
24
 
23
25
  const meta = {
24
26
  title: 'SVG <image> must have a text alternative',
25
- description: 'Checks that SVG <image> elements provide a text alternative via <title>/<desc> or an ARIA accessible name.',
27
+ description:
28
+ 'Checks that SVG <image> elements provide a text alternative via <title>/<desc> or an ARIA accessible name.',
26
29
  i18n: {
27
30
  titleKey: 'svgImage_textAltPresent_title',
28
31
  descriptionKey: 'svgImage_textAltPresent_description'
@@ -31,7 +34,13 @@ const meta = {
31
34
  tags: ['wcag2a', 'wcag111', 'nontext', 'svg', 'image', 'atomic', 'automatic'],
32
35
  wcagSc: ['1.1.1'],
33
36
  normativeMappings: [
34
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
37
+ {
38
+ standard: 'WCAG',
39
+ version: '2.2',
40
+ requirement: '1.1.1',
41
+ title: 'Non-text Content',
42
+ conformanceLevel: 'A'
43
+ }
35
44
  ],
36
45
  defaultSeverity: 'serious',
37
46
  category: 'perceivable',
@@ -48,31 +57,38 @@ function runInPage(ctx) {
48
57
  const { document, root, helpers, rule } = ctx;
49
58
  const safeRoot = root || document;
50
59
 
51
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
52
- const queryAll = helpers && typeof helpers.queryAll === 'function'
53
- ? helpers.queryAll
54
- : (sel) => {
55
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
56
- catch { return []; }
57
- };
58
-
59
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
60
- ? helpers.getEligibilityInfo
61
- : null;
62
-
63
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
64
- ? helpers.isAccTreeEligible
65
- : null;
66
-
67
- const isFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
68
- ? helpers.getFocusableInfo
69
- : null;
70
-
71
- const getAccessibleNameInfo = helpers && typeof helpers.getAccessibleNameInfo === 'function'
72
- ? helpers.getAccessibleNameInfo
73
- : null;
74
-
75
- function trim(v) { return (v == null ? '' : String(v)).trim(); }
60
+ const queryAllSmart =
61
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
62
+ const queryAll =
63
+ helpers && typeof helpers.queryAll === 'function'
64
+ ? helpers.queryAll
65
+ : (sel) => {
66
+ try {
67
+ return safeRoot && safeRoot.querySelectorAll
68
+ ? Array.from(safeRoot.querySelectorAll(sel))
69
+ : [];
70
+ } catch {
71
+ return [];
72
+ }
73
+ };
74
+
75
+ const getEligibilityInfo =
76
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
77
+
78
+ const isAccTreeEligible =
79
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
80
+
81
+ const isFocusableInfo =
82
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
83
+
84
+ const getAccessibleNameInfo =
85
+ helpers && typeof helpers.getAccessibleNameInfo === 'function'
86
+ ? helpers.getAccessibleNameInfo
87
+ : null;
88
+
89
+ function trim(v) {
90
+ return (v == null ? '' : String(v)).trim();
91
+ }
76
92
 
77
93
  // Per SVG accessible-name conventions, only a <title> that is literally
78
94
  // the first child element is used by assistive technologies as the
@@ -116,16 +132,27 @@ function runInPage(ctx) {
116
132
  }
117
133
 
118
134
  const rawImages = (() => {
119
- try { return Array.from((queryAllSmart ? queryAllSmart('image') : queryAll('image')) || []); }
120
- catch { return queryAll('svg image'); }
135
+ try {
136
+ return Array.from((queryAllSmart ? queryAllSmart('image') : queryAll('image')) || []);
137
+ } catch {
138
+ return queryAll('svg image');
139
+ }
121
140
  })();
122
141
 
123
142
  const images = rawImages.filter((el) => {
124
- try { return el && (el.namespaceURI === 'http://www.w3.org/2000/svg') && (String(el.localName).toLowerCase() === 'image'); }
125
- catch { return false; }
143
+ try {
144
+ return (
145
+ el &&
146
+ el.namespaceURI === 'http://www.w3.org/2000/svg' &&
147
+ String(el.localName).toLowerCase() === 'image'
148
+ );
149
+ } catch {
150
+ return false;
151
+ }
126
152
  });
127
153
 
128
- if (!images.length) return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
154
+ if (!images.length)
155
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
129
156
 
130
157
  const occurrences = [];
131
158
  let applicableCount = 0;
@@ -135,20 +162,31 @@ function runInPage(ctx) {
135
162
 
136
163
  if (isAccTreeEligible) {
137
164
  const elig = (() => {
138
- try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
165
+ try {
166
+ return isAccTreeEligible(el, ctx);
167
+ } catch {
168
+ return { eligible: true, reasons: [] };
169
+ }
139
170
  })();
140
171
  if (elig && elig.eligible === false) continue;
141
172
  }
142
173
 
143
174
  const role = trim(el.getAttribute('role')).toLowerCase();
144
175
  if (role === 'presentation' || role === 'none') {
145
- let focusable = false;
176
+ let focusable;
146
177
  if (isFocusableInfo) {
147
- const fi = (() => { try { return isFocusableInfo(el, ctx); } catch { return null; } })();
178
+ const fi = (() => {
179
+ try {
180
+ return isFocusableInfo(el, ctx);
181
+ } catch {
182
+ return null;
183
+ }
184
+ })();
148
185
  focusable = !!(fi && fi.focusable);
149
186
  } else {
150
187
  const tabindex = el.getAttribute('tabindex');
151
- focusable = tabindex != null && trim(tabindex) !== '' && !Number.isNaN(Number(trim(tabindex)));
188
+ focusable =
189
+ tabindex != null && trim(tabindex) !== '' && !Number.isNaN(Number(trim(tabindex)));
152
190
  }
153
191
  if (!focusable) continue;
154
192
  }
@@ -162,25 +200,49 @@ function runInPage(ctx) {
162
200
  if (desc) continue;
163
201
 
164
202
  // Only now check “accessible name” (but scoped to allowed mechanisms)
165
- const ariaLabelRaw = (() => { try { return el.getAttribute('aria-label'); } catch { return null; } })();
166
- const ariaLabelledbyRaw = (() => { try { return el.getAttribute('aria-labelledby'); } catch { return null; } })();
167
- const titleAttrRaw = (() => { try { return el.getAttribute('title'); } catch { return null; } })();
203
+ const ariaLabelRaw = (() => {
204
+ try {
205
+ return el.getAttribute('aria-label');
206
+ } catch {
207
+ return null;
208
+ }
209
+ })();
210
+ const ariaLabelledbyRaw = (() => {
211
+ try {
212
+ return el.getAttribute('aria-labelledby');
213
+ } catch {
214
+ return null;
215
+ }
216
+ })();
217
+ const titleAttrRaw = (() => {
218
+ try {
219
+ return el.getAttribute('title');
220
+ } catch {
221
+ return null;
222
+ }
223
+ })();
168
224
 
169
225
  const ariaLabel = trim(ariaLabelRaw);
170
226
  const ariaLabelledby = trim(ariaLabelledbyRaw);
171
227
  const titleAttr = trim(titleAttrRaw);
172
228
 
173
229
  const hasMechanism =
174
- (ariaLabelRaw !== null && ariaLabel.length > 0) ||
175
- (ariaLabelledbyRaw !== null && ariaLabelledby.length > 0) ||
176
- (titleAttrRaw !== null && titleAttr.length > 0);
230
+ (ariaLabelRaw !== null && ariaLabel.length > 0) ||
231
+ (ariaLabelledbyRaw !== null && ariaLabelledby.length > 0) ||
232
+ (titleAttrRaw !== null && titleAttr.length > 0);
177
233
 
178
- let nameInfo = null;
234
+ let nameInfo;
179
235
  let hasName = false;
180
236
 
181
237
  if (hasMechanism) {
182
238
  if (getAccessibleNameInfo) {
183
- nameInfo = (() => { try { return getAccessibleNameInfo(el, ctx); } catch { return null; } })();
239
+ nameInfo = (() => {
240
+ try {
241
+ return getAccessibleNameInfo(el, ctx);
242
+ } catch {
243
+ return null;
244
+ }
245
+ })();
184
246
  hasName = !!(nameInfo && nameInfo.present && trim(nameInfo.value));
185
247
  } else {
186
248
  // Without helper, accept presence of non-empty allowed attributes
@@ -212,9 +274,16 @@ function runInPage(ctx) {
212
274
  }
213
275
  }
214
276
 
215
- if (applicableCount === 0) return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
216
- if (!occurrences.length) return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
217
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
277
+ if (applicableCount === 0)
278
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
279
+ if (!occurrences.length)
280
+ return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
281
+ return {
282
+ ruleId: rule.ruleId,
283
+ outcome: 'fail',
284
+ severity: rule.defaultSeverity || 'minor',
285
+ occurrences
286
+ };
218
287
  }
219
288
 
220
289
  module.exports = { id, meta, runInPage };