@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
  /**
@@ -20,25 +22,17 @@
20
22
  * display orientation unless that orientation is essential.
21
23
  * @implementation-notes
22
24
  * - The rotation DEGREE is what makes this the exploit signature, not
23
- * merely the presence of a `rotate()` function — verified against a
24
- * widely-used reference engine's own `css-orientation-lock` check
25
- * (`cssOrientationLockEvaluate`), which computes the actual rotation angle and only
26
- * flags ~90/~270 degrees, explicitly excluding ~0/~180 (a no-op or a
27
- * flip, neither of which changes portrait<->landscape). A prior
28
- * version of this rule flagged the bare presence of "rotate(" in an
29
- * orientation-media-query rule regardless of angle — a real, confirmed
30
- * false positive found via the live-DOM cross-engine corpus 2026-07-23
31
- * on a real page (Times of India) whose `@media (orientation:portrait)`
32
- * block only rotated a small decorative arrow icon 45 degrees, nothing
33
- * resembling a page-orientation lock.
25
+ * merely the presence of a `rotate()` function: a small decorative icon
26
+ * rotated 45 degrees inside an orientation media query is not a page
27
+ * lock. Compute the actual rotation angle and only flag ~90/~270 degrees,
28
+ * excluding ~0/~180 (a no-op or a flip, neither of which changes
29
+ * portrait<->landscape).
34
30
  * - Only `rotate`/`rotateZ` (transform functions) and the standalone CSS
35
- * `rotate` property are parsed for degrees — matches that reference engine's
36
- * own two sources (`style.transform`/`style.rotate`) but, like that engine,
37
- * does not attempt to decompose `matrix()`/`matrix3d()`/`rotate3d()` into an
38
- * equivalent angle (that engine does handle those; deliberately deferred here
39
- * as the same class of "genuinely more complex, lower real-world
40
- * value" work already deferred elsewhere in this engine, e.g.
41
- * `table-th-has-data-cells`'s narrower positional-header algorithm).
31
+ * `rotate` property are parsed for degrees, from the two sources
32
+ * `style.transform`/`style.rotate`; `matrix()`/`matrix3d()`/`rotate3d()`
33
+ * are not decomposed into an equivalent angle (deliberately deferred as
34
+ * the same class of higher-complexity/lower-value work deferred elsewhere,
35
+ * e.g. `table-th-has-data-cells`'s narrower positional-header algorithm).
42
36
  * - Cross-origin stylesheets throw on `.cssRules` access (browser
43
37
  * security model) and are skipped — same class of limitation as any
44
38
  * check that can only see same-origin/inspectable content (compare
@@ -64,7 +58,13 @@ const meta = {
64
58
  tags: ['wcag21aa', 'wcag134', 'structure', 'atomic', 'automatic'],
65
59
  wcagSc: ['1.3.4'],
66
60
  normativeMappings: [
67
- { standard: 'WCAG', version: '2.2', requirement: '1.3.4', title: 'Orientation', conformanceLevel: 'AA' }
61
+ {
62
+ standard: 'WCAG',
63
+ version: '2.2',
64
+ requirement: '1.3.4',
65
+ title: 'Orientation',
66
+ conformanceLevel: 'AA'
67
+ }
68
68
  ],
69
69
  defaultSeverity: 'serious',
70
70
  category: 'perceivable',
@@ -87,12 +87,14 @@ function runInPage(ctx) {
87
87
  const CSS_MEDIA_RULE = 4;
88
88
  const CSS_STYLE_RULE = 1;
89
89
 
90
- function trim(v) { return (v == null ? '' : String(v)).trim(); }
90
+ function trim(v) {
91
+ return (v == null ? '' : String(v)).trim();
92
+ }
91
93
 
92
94
  // Converts a rotate()/rotateZ() angle argument (with unit) to degrees.
93
- // Matches a widely-used reference engine's own getAngleInDegrees: only deg/grad/rad/turn are
94
- // recognized; a unitless or unrecognized value contributes 0 (ignored,
95
- // not treated as a lock -- an unparseable angle isn't a confirmed exploit).
95
+ // Only deg/grad/rad/turn are recognized; a unitless or unrecognized value
96
+ // contributes 0 (ignored, not treated as a lock -- an unparseable angle
97
+ // isn't a confirmed exploit).
96
98
  function angleToDegrees(raw) {
97
99
  const m = /(-?[\d.]+)\s*(deg|grad|rad|turn)/i.exec(raw);
98
100
  if (!m) return 0;
@@ -119,16 +121,20 @@ function runInPage(ctx) {
119
121
  return total;
120
122
  }
121
123
 
122
- // Whole-page-orientation-lock detection, matching a widely-used reference engine's own degree
123
- // math exactly (see @implementation-notes): a rotation near 0 or 180
124
- // degrees (mod 180) is a no-op or a flip, neither of which changes
125
- // portrait<->landscape, so it's explicitly NOT a lock; only a rotation
126
- // near 90 or 270 degrees (mod 90, once the 0/180 case is excluded) is.
124
+ // Whole-page-orientation-lock detection (see @implementation-notes): a
125
+ // rotation near 0 or 180 degrees (mod 180) is a no-op or a flip, neither
126
+ // of which changes portrait<->landscape, so it's NOT a lock; only a
127
+ // rotation near 90 or 270 degrees (mod 90, once the 0/180 case is
128
+ // excluded) is.
127
129
  function isLockingRotation(styleDecl) {
128
130
  if (!styleDecl) return false;
129
- const transformVal = trim(styleDecl.transform) || trim(styleDecl.getPropertyValue ? styleDecl.getPropertyValue('-webkit-transform') : '');
130
- // The standalone CSS `rotate` property (distinct from `transform: rotate()`) -- that reference engine checks this too.
131
- const rotatePropVal = trim(styleDecl.getPropertyValue ? styleDecl.getPropertyValue('rotate') : '');
131
+ const transformVal =
132
+ trim(styleDecl.transform) ||
133
+ trim(styleDecl.getPropertyValue ? styleDecl.getPropertyValue('-webkit-transform') : '');
134
+ // The standalone CSS `rotate` property (distinct from `transform: rotate()`).
135
+ const rotatePropVal = trim(
136
+ styleDecl.getPropertyValue ? styleDecl.getPropertyValue('rotate') : ''
137
+ );
132
138
 
133
139
  let degrees = rotateDegreesFromTransform(transformVal);
134
140
  if (!degrees && rotatePropVal) degrees = angleToDegrees(rotatePropVal);
@@ -189,9 +195,10 @@ function runInPage(ctx) {
189
195
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
190
196
  }
191
197
 
192
- const target = document.documentElement || (document.body || null);
198
+ const target = document.documentElement || document.body || null;
193
199
  const stableSelector = helpers.buildSelector && target ? helpers.buildSelector(target) : 'html';
194
- const html = helpers.getOuterHtmlSnippet && target ? helpers.getOuterHtmlSnippet(target) : '<html>';
200
+ const html =
201
+ helpers.getOuterHtmlSnippet && target ? helpers.getOuterHtmlSnippet(target) : '<html>';
195
202
 
196
203
  const occurrences = findings.map((f) => ({
197
204
  selector: stableSelector,
@@ -204,11 +211,20 @@ function runInPage(ctx) {
204
211
  params: { mediaText: f.mediaText, selectorText: f.selectorText }
205
212
  },
206
213
  data: {
207
- details: { reasonCode: 'ORIENTATION_MEDIA_ROTATE_TRANSFORM', mediaText: f.mediaText, selectorText: f.selectorText }
214
+ details: {
215
+ reasonCode: 'ORIENTATION_MEDIA_ROTATE_TRANSFORM',
216
+ mediaText: f.mediaText,
217
+ selectorText: f.selectorText
218
+ }
208
219
  }
209
220
  }));
210
221
 
211
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
222
+ return {
223
+ ruleId: rule.ruleId,
224
+ outcome: 'fail',
225
+ severity: rule.defaultSeverity || 'serious',
226
+ occurrences
227
+ };
212
228
  }
213
229
 
214
230
  module.exports = { id, meta, runInPage, applicability };
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -18,12 +20,11 @@
18
20
  * violation (see implementation-notes). Any other direct or wrapped child
19
21
  * breaks the description-list semantics assistive technologies rely on.
20
22
  * @implementation-notes
21
- * - Only one level of <div> wrapping is flattened, matching a widely-used
22
- * reference engine's definition-list algorithm — a <div> nested inside
23
+ * - Only one level of <div> wrapping is flattened — a <div> nested inside
23
24
  * another wrapping <div> is not flattened further and its contents are
24
25
  * reported invalid.
25
- * - Verified 2026-07-20 against a widely-used reference engine: the dt/dd pairing is
26
- * only required "when not empty". A flattened set with NEITHER dt nor dd
26
+ * - The dt/dd pairing is only required "when not empty". A flattened set
27
+ * with NEITHER dt nor dd
27
28
  * — whether from an empty wrapping <div>, only <script>/<template>/
28
29
  * <style> content, or a genuinely childless <dl> — is not flagged; only
29
30
  * an unbalanced dt/dd pairing is a real structural problem.
@@ -35,7 +36,8 @@ const id = 'definition-list-children-valid';
35
36
 
36
37
  const meta = {
37
38
  title: 'Description lists must be structured correctly',
38
- description: 'Checks that <dl> elements only directly contain <dt>/<dd> groups (optionally wrapped in one <div>), <script>, <template>, or <style>.',
39
+ description:
40
+ 'Checks that <dl> elements only directly contain <dt>/<dd> groups (optionally wrapped in one <div>), <script>, <template>, or <style>.',
39
41
  i18n: {
40
42
  titleKey: 'definitionListChildrenValid_title',
41
43
  descriptionKey: 'definitionListChildrenValid_description'
@@ -44,7 +46,13 @@ const meta = {
44
46
  tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
45
47
  wcagSc: ['1.3.1'],
46
48
  normativeMappings: [
47
- { standard: 'WCAG', version: '2.2', requirement: '1.3.1', title: 'Info and Relationships', conformanceLevel: 'A' }
49
+ {
50
+ standard: 'WCAG',
51
+ version: '2.2',
52
+ requirement: '1.3.1',
53
+ title: 'Info and Relationships',
54
+ conformanceLevel: 'A'
55
+ }
48
56
  ],
49
57
  defaultSeverity: 'serious',
50
58
  category: 'perceivable',
@@ -54,7 +62,7 @@ const meta = {
54
62
  };
55
63
 
56
64
  function runInPage(ctx) {
57
- const { document, helpers, rule } = ctx;
65
+ const { helpers, rule } = ctx;
58
66
 
59
67
  // Declared inside runInPage — see scripts/build-core.js header
60
68
  // ("runInPage MUST be self-contained").
@@ -89,31 +97,39 @@ function runInPage(ctx) {
89
97
  const invalidTags = [];
90
98
  for (const node of flattened) {
91
99
  const tag = node.tagName.toLowerCase();
92
- if (tag === 'dt') { hasDt = true; continue; }
93
- if (tag === 'dd') { hasDd = true; continue; }
100
+ if (tag === 'dt') {
101
+ hasDt = true;
102
+ continue;
103
+ }
104
+ if (tag === 'dd') {
105
+ hasDd = true;
106
+ continue;
107
+ }
94
108
  if (!PASSTHROUGH_TAGS.has(tag)) invalidTags.push(tag);
95
109
  }
110
+ const dedupedInvalidTags = [...new Set(invalidTags)];
96
111
 
97
- // Verified 2026-07-20 against a widely-used reference engine's definition-list check:
98
- // the dt/dd pairing is only required "when not empty" — a <dl> with
112
+ // The dt/dd pairing is only required "when not empty" — a <dl> with
99
113
  // NEITHER dt nor dd (whether genuinely childless after flattening, only
100
114
  // passthrough script/template/style content, or an empty wrapping div)
101
115
  // is vacuously fine, not a violation. Only an UNBALANCED pairing (dt
102
116
  // present without any dd, or vice versa) is a real structural problem.
103
117
  const reasonCode = invalidTags.length
104
- ? 'DL_INVALID_CHILD'
105
- : ((hasDt || hasDd) && !(hasDt && hasDd) ? 'DL_NO_DT_DD' : null);
118
+ ? 'DL_INVALID_CHILD'
119
+ : (hasDt || hasDd) && !(hasDt && hasDd)
120
+ ? 'DL_NO_DT_DD'
121
+ : null;
106
122
  if (!reasonCode) continue;
107
123
 
108
124
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
109
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
125
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
110
126
 
111
127
  const summary = invalidTags.length
112
- ? 'This description list contains a direct or wrapped child that is not part of a dt/dd group.'
113
- : 'This description list has no <dt>/<dd> term-definition group.';
128
+ ? 'This description list contains a direct or wrapped child that is not part of a dt/dd group.'
129
+ : 'This description list has no <dt>/<dd> term-definition group.';
114
130
  const hint = invalidTags.length
115
- ? 'Only use <dt>/<dd> (optionally wrapped in one <div>), <script>, <template>, or <style> inside <dl>.'
116
- : 'Add at least one <dt>/<dd> pair inside this <dl>.';
131
+ ? 'Only use <dt>/<dd> (optionally wrapped in one <div>), <script>, <template>, or <style> inside <dl>.'
132
+ : 'Add at least one <dt>/<dd> pair inside this <dl>.';
117
133
 
118
134
  occurrences.push({
119
135
  selector: stableSelector,
@@ -122,12 +138,12 @@ function runInPage(ctx) {
122
138
  hint,
123
139
  i18n: {
124
140
  summaryKey: invalidTags.length
125
- ? 'definitionListChildrenValid_summary_fail_invalidChild'
126
- : 'definitionListChildrenValid_summary_fail_noDtDd',
141
+ ? 'definitionListChildrenValid_summary_fail_invalidChild'
142
+ : 'definitionListChildrenValid_summary_fail_noDtDd',
127
143
  hintKey: invalidTags.length
128
- ? 'definitionListChildrenValid_hint_fail_invalidChild'
129
- : 'definitionListChildrenValid_hint_fail_noDtDd',
130
- params: { invalidChildren: invalidTags.join(', ') }
144
+ ? 'definitionListChildrenValid_hint_fail_invalidChild'
145
+ : 'definitionListChildrenValid_hint_fail_noDtDd',
146
+ params: { invalidChildren: dedupedInvalidTags.join(', ') }
131
147
  },
132
148
  data: {
133
149
  details: { reasonCode, invalidChildren: invalidTags }
@@ -139,9 +155,14 @@ function runInPage(ctx) {
139
155
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
140
156
  }
141
157
  if (occurrences.length) {
142
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
158
+ return {
159
+ ruleId: rule.ruleId,
160
+ outcome: 'fail',
161
+ severity: rule.defaultSeverity || 'serious',
162
+ occurrences
163
+ };
143
164
  }
144
165
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
145
166
  }
146
167
 
147
- module.exports = { id, meta, runInPage };
168
+ module.exports = { id, meta, runInPage };
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -14,8 +16,8 @@
14
16
  * @expectation
15
17
  * Neither element is present. Since their movement can never be paused,
16
18
  * stopped, or hidden by the user, presence is itself the violation — this
17
- * rule has no partial-pass case, matching a widely-used reference engine's `blink`/`marquee`
18
- * rules (which report only when the element is found).
19
+ * rule has no partial-pass case (it reports only when the element is
20
+ * found).
19
21
  * @implementation-notes
20
22
  * - Not rule-gated on isAccTreeEligible: presence in markup is itself the
21
23
  * violation, independent of visibility (moving/blinking content inside a
@@ -29,7 +31,8 @@ const id = 'deprecated-elements-not-used';
29
31
 
30
32
  const meta = {
31
33
  title: 'Obsolete non-stoppable elements (<blink>, <marquee>) must not be used',
32
- description: 'Checks that deprecated, non-standard HTML elements whose blinking/scrolling content cannot be paused, stopped, or hidden by the user (<blink>, <marquee>) are not present.',
34
+ description:
35
+ 'Checks that deprecated, non-standard HTML elements whose blinking/scrolling content cannot be paused, stopped, or hidden by the user (<blink>, <marquee>) are not present.',
33
36
  i18n: {
34
37
  titleKey: 'deprecatedElements_title',
35
38
  descriptionKey: 'deprecatedElements_description'
@@ -38,7 +41,13 @@ const meta = {
38
41
  tags: ['wcag2a', 'wcag222', 'structure', 'atomic', 'automatic'],
39
42
  wcagSc: ['2.2.2'],
40
43
  normativeMappings: [
41
- { standard: 'WCAG', version: '2.2', requirement: '2.2.2', title: 'Pause, Stop, Hide', conformanceLevel: 'A' }
44
+ {
45
+ standard: 'WCAG',
46
+ version: '2.2',
47
+ requirement: '2.2.2',
48
+ title: 'Pause, Stop, Hide',
49
+ conformanceLevel: 'A'
50
+ }
42
51
  ],
43
52
  defaultSeverity: 'serious',
44
53
  category: 'operable',
@@ -48,9 +57,11 @@ const meta = {
48
57
  };
49
58
 
50
59
  function runInPage(ctx) {
51
- const { document, helpers, rule } = ctx;
60
+ const { helpers, rule } = ctx;
52
61
 
53
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('blink, marquee') : helpers.queryAll('blink, marquee');
62
+ const nodes = helpers.queryAllSmart
63
+ ? helpers.queryAllSmart('blink, marquee')
64
+ : helpers.queryAll('blink, marquee');
54
65
 
55
66
  const occurrences = [];
56
67
  let applicableCount = 0;
@@ -62,7 +73,7 @@ function runInPage(ctx) {
62
73
 
63
74
  const tag = el.tagName.toLowerCase();
64
75
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
65
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
76
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
66
77
 
67
78
  occurrences.push({
68
79
  selector: stableSelector,
@@ -84,9 +95,14 @@ function runInPage(ctx) {
84
95
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
85
96
  }
86
97
  if (occurrences.length) {
87
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
98
+ return {
99
+ ruleId: rule.ruleId,
100
+ outcome: 'fail',
101
+ severity: rule.defaultSeverity || 'serious',
102
+ occurrences
103
+ };
88
104
  }
89
105
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
90
106
  }
91
107
 
92
- module.exports = { id, meta, runInPage };
108
+ module.exports = { id, meta, runInPage };
@@ -1,19 +1,28 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'dialog-name-present';
4
6
 
5
7
  const meta = {
6
8
  title: 'Dialogs have an accessible name',
7
- description: 'Checks that elements with role="dialog" or role="alertdialog" expose a non-empty accessible name.',
9
+ description:
10
+ 'Checks that elements with role="dialog" or role="alertdialog" expose a non-empty accessible name.',
8
11
  i18n: {
9
12
  titleKey: 'dialogNamePresent_title',
10
13
  descriptionKey: 'dialogNamePresent_description'
11
14
  },
12
15
  helpUrl: null,
13
- tags: ['wcag2a','wcag412','structure','atomic','automatic','name','dialog'],
16
+ tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic', 'name', 'dialog'],
14
17
  wcagSc: ['4.1.2'],
15
18
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
19
+ {
20
+ standard: 'WCAG',
21
+ version: '2.2',
22
+ requirement: '4.1.2',
23
+ title: 'Name, Role, Value',
24
+ conformanceLevel: 'A'
25
+ }
17
26
  ],
18
27
  defaultSeverity: 'serious',
19
28
  category: 'robust',
@@ -24,69 +33,22 @@ const meta = {
24
33
 
25
34
  function runInPage(ctx) {
26
35
  const { document, helpers, rule } = ctx;
27
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
29
- : null;
30
-
36
+ const getEligibilityInfo =
37
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
31
38
 
32
39
  function normalizeWs(s) {
33
- return String(s || '').replace(/\s+/g, ' ').trim();
40
+ return String(s || '')
41
+ .replace(/\s+/g, ' ')
42
+ .trim();
34
43
  }
35
44
 
36
45
  function getAttr(el, name) {
37
46
  try {
38
47
  if (!el || !el.getAttribute) return '';
39
48
  return normalizeWs(el.getAttribute(name));
40
- } catch { return ''; }
41
- }
42
-
43
- function hasAttr(el, name) {
44
- try { return !!(el && el.hasAttribute && el.hasAttribute(name)); } catch { return false; }
45
- }
46
-
47
- function isExplicitProgrammatic(el) {
48
- // Programmatic name mechanisms we treat as authoritative (presence-only):
49
- // - aria-label (non-empty)
50
- // - aria-labelledby (non-empty)
51
- // - title (non-empty) [weak but allowed for presence]
52
- const al = getAttr(el, 'aria-label');
53
- if (al) return true;
54
- const alb = getAttr(el, 'aria-labelledby');
55
- if (alb) return true;
56
- const t = getAttr(el, 'title');
57
- if (t) return true;
58
- return false;
59
- }
60
-
61
- function buildLabelForMap(doc) {
62
- const map = new Map(); // id -> label element (first)
63
- try {
64
- const labels = doc && doc.getElementsByTagName ? doc.getElementsByTagName('label') : [];
65
- for (let i = 0; i < labels.length; i += 1) {
66
- const lab = labels[i];
67
- if (!lab || !lab.getAttribute) continue;
68
- const f = normalizeWs(lab.getAttribute('for'));
69
- if (!f) continue;
70
- if (!map.has(f)) map.set(f, lab);
71
- }
72
- } catch {}
73
- return map;
74
- }
75
-
76
- function getConservativeSubtreeText(document, container) {
77
- // "Name from content" — recurses into descendants and uses each one's
78
- // own accessible name (img alt, aria-label/aria-labelledby, title) when
79
- // it has one, not just literal text nodes. See getContentNameInfo's
80
- // header comment in src/core/dom-helpers.js for the full rationale
81
- // (this replaced a text-node-only TreeWalker that missed the common
82
- // "<a><img alt='...'></a>" logo-link / "<button><img alt='...'></button>"
83
- // icon-button pattern).
84
- if (helpers.getContentNameInfo) {
85
- const info = helpers.getContentNameInfo(container, ctx);
86
- return info && info.present ? info.value : '';
49
+ } catch {
50
+ return '';
87
51
  }
88
- const t = (container && container.textContent) ? String(container.textContent) : '';
89
- return t.replace(/\s+/g, ' ').trim();
90
52
  }
91
53
 
92
54
  function resolveAriaLabelledbyText(document, el, maxRefs) {
@@ -95,16 +57,14 @@ function runInPage(ctx) {
95
57
  // Resolve via the shared getTextFromIdRefs helper — computes each
96
58
  // referenced element's own ACCESSIBLE NAME (aria-label, then
97
59
  // aria-labelledby, then a value-like name, then content, then title),
98
- // not just its content text. Found via a real page (BBC News' cookie-
99
- // consent dialog, 2026-07-22): aria-labelledby pointed at an
100
- // <iframe title="SP Consent Message">, whose only name source is its
101
- // title attribute (an iframe's content is opaque/cross-origin per
102
- // HTML-AAM, so "name from content" is always empty). The previous
103
- // version here only ever computed name-from-content of the referenced
104
- // node (via getConservativeSubtreeText), silently missing the title
105
- // fallback and reporting no accessible name at all — the identical
106
- // pattern was hand-copied into 15 other *-name-present rules; all
107
- // fixed the same way.
60
+ // not just its content text. This matters when aria-labelledby points
61
+ // at an <iframe title="..."> (e.g. BBC News' cookie-consent dialog,
62
+ // pointing at an `<iframe title="SP Consent Message">`), whose only
63
+ // name source is its title attribute — an iframe's content is
64
+ // opaque/cross-origin per HTML-AAM, so "name from content" is always
65
+ // empty. Computing only name-from-content of the referenced node would
66
+ // silently miss the title fallback and report no accessible name at
67
+ // all, the same pattern every other *-name-present rule guards against.
108
68
  if (helpers.getTextFromIdRefs) {
109
69
  try {
110
70
  const r = helpers.getTextFromIdRefs(raw, ctx, { maxRefs: maxRefs || 8 });
@@ -114,17 +74,9 @@ function runInPage(ctx) {
114
74
  return '';
115
75
  }
116
76
 
117
- function getInputValueName(el) {
118
- try {
119
- const type = getAttr(el, 'type').toLowerCase();
120
- if (type !== 'button' && type !== 'submit' && type !== 'reset') return '';
121
- const v = getAttr(el, 'value');
122
- return v;
123
- } catch { return ''; }
124
- }
125
-
126
77
  function isEligibleAcc(helpers, el, ctx) {
127
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
78
+ const fn =
79
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
128
80
  if (!fn) return true;
129
81
  try {
130
82
  const r = fn(el, ctx);
@@ -135,13 +87,13 @@ function runInPage(ctx) {
135
87
  }
136
88
  }
137
89
 
138
-
139
90
  const occurrences = [];
140
91
  let applicableCount = 0;
141
92
 
142
93
  const selector = '[role="dialog"],[role="alertdialog"]';
143
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
144
-
94
+ const nodes = helpers.queryAllSmart
95
+ ? helpers.queryAllSmart(selector)
96
+ : helpers.queryAll(selector);
145
97
 
146
98
  function evaluate(el) {
147
99
  const ariaLabel = getAttr(el, 'aria-label');
@@ -173,10 +125,16 @@ function runInPage(ctx) {
173
125
  if (res.ok) continue;
174
126
 
175
127
  const eligInfo = getEligibilityInfo
176
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
177
- : null;
128
+ ? (() => {
129
+ try {
130
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
131
+ } catch {
132
+ return null;
133
+ }
134
+ })()
135
+ : null;
178
136
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
179
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
137
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
180
138
 
181
139
  occurrences.push({
182
140
  selector: stableSelector,
@@ -195,12 +153,16 @@ function runInPage(ctx) {
195
153
  });
196
154
  }
197
155
 
198
-
199
156
  if (applicableCount === 0) {
200
157
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
201
158
  }
202
159
  if (occurrences.length) {
203
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
160
+ return {
161
+ ruleId: rule.ruleId,
162
+ outcome: 'fail',
163
+ severity: rule.defaultSeverity || 'minor',
164
+ occurrences
165
+ };
204
166
  }
205
167
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
206
168
  }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -23,7 +25,8 @@ const id = 'dlitem-parent-valid';
23
25
 
24
26
  const meta = {
25
27
  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>.',
28
+ description:
29
+ 'Checks that <dt>/<dd> elements are contained by a <dl>, directly or via one wrapping <div>.',
27
30
  i18n: {
28
31
  titleKey: 'dlitemParentValid_title',
29
32
  descriptionKey: 'dlitemParentValid_description'
@@ -32,7 +35,13 @@ const meta = {
32
35
  tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'automatic', 'list'],
33
36
  wcagSc: ['1.3.1'],
34
37
  normativeMappings: [
35
- { standard: 'WCAG', version: '2.2', requirement: '1.3.1', title: 'Info and Relationships', conformanceLevel: 'A' }
38
+ {
39
+ standard: 'WCAG',
40
+ version: '2.2',
41
+ requirement: '1.3.1',
42
+ title: 'Info and Relationships',
43
+ conformanceLevel: 'A'
44
+ }
36
45
  ],
37
46
  defaultSeverity: 'serious',
38
47
  category: 'perceivable',
@@ -42,9 +51,11 @@ const meta = {
42
51
  };
43
52
 
44
53
  function runInPage(ctx) {
45
- const { document, helpers, rule } = ctx;
54
+ const { helpers, rule } = ctx;
46
55
 
47
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dt, dd') : helpers.queryAll('dt, dd');
56
+ const nodes = helpers.queryAllSmart
57
+ ? helpers.queryAllSmart('dt, dd')
58
+ : helpers.queryAll('dt, dd');
48
59
 
49
60
  const occurrences = [];
50
61
  let applicableCount = 0;
@@ -61,7 +72,8 @@ function runInPage(ctx) {
61
72
 
62
73
  if (!valid && parentTag === 'div') {
63
74
  const grandparent = parent.parentElement;
64
- const grandparentTag = grandparent && grandparent.tagName ? grandparent.tagName.toLowerCase() : '';
75
+ const grandparentTag =
76
+ grandparent && grandparent.tagName ? grandparent.tagName.toLowerCase() : '';
65
77
  valid = grandparentTag === 'dl';
66
78
  }
67
79
 
@@ -69,7 +81,7 @@ function runInPage(ctx) {
69
81
 
70
82
  const tag = el.tagName.toLowerCase();
71
83
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
72
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
84
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
73
85
 
74
86
  occurrences.push({
75
87
  selector: stableSelector,
@@ -91,9 +103,14 @@ function runInPage(ctx) {
91
103
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
92
104
  }
93
105
  if (occurrences.length) {
94
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
106
+ return {
107
+ ruleId: rule.ruleId,
108
+ outcome: 'fail',
109
+ severity: rule.defaultSeverity || 'serious',
110
+ occurrences
111
+ };
95
112
  }
96
113
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
97
114
  }
98
115
 
99
- module.exports = { id, meta, runInPage };
116
+ module.exports = { id, meta, runInPage };