@surea11y/core 1.3.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 (163) hide show
  1. package/CHANGELOG.md +46 -2
  2. package/README.md +109 -35
  3. package/bin/surea11y-core.js +20 -0
  4. package/docs/API_STABILITY.md +26 -0
  5. package/docs/CI_INTEGRATIONS.md +7 -7
  6. package/docs/ENGINE_OPTIONS.md +1 -1
  7. package/docs/I18N.md +12 -9
  8. package/docs/INTEGRATION.md +1 -1
  9. package/docs/LIMITATIONS.md +1 -1
  10. package/docs/REPORT.md +1 -1
  11. package/package.json +50 -16
  12. package/src/baseline.js +0 -0
  13. package/src/checks/automatic/area-alt-present.js +4 -6
  14. package/src/checks/automatic/aria-allowed-attr.js +15 -51
  15. package/src/checks/automatic/aria-allowed-role.js +2 -0
  16. package/src/checks/automatic/aria-braille-equivalent.js +2 -0
  17. package/src/checks/automatic/aria-conditional-attr.js +8 -7
  18. package/src/checks/automatic/aria-deprecated-role.js +4 -3
  19. package/src/checks/automatic/aria-hidden-body.js +6 -4
  20. package/src/checks/automatic/aria-hidden-focus.js +12 -13
  21. package/src/checks/automatic/aria-prohibited-attr.js +98 -105
  22. package/src/checks/automatic/aria-prohibited-children.js +56 -87
  23. package/src/checks/automatic/aria-required-attr.js +6 -7
  24. package/src/checks/automatic/aria-required-children.js +7 -10
  25. package/src/checks/automatic/aria-required-parent.js +20 -25
  26. package/src/checks/automatic/aria-role-name-present.js +2 -0
  27. package/src/checks/automatic/aria-roles-valid.js +2 -0
  28. package/src/checks/automatic/aria-valid-attr-value.js +18 -15
  29. package/src/checks/automatic/aria-valid-attr.js +2 -0
  30. package/src/checks/automatic/autocomplete-valid.js +2 -0
  31. package/src/checks/automatic/avoid-inline-spacing.js +3 -2
  32. package/src/checks/automatic/binary-control-name-present.js +2 -0
  33. package/src/checks/automatic/button-name-present.js +7 -7
  34. package/src/checks/automatic/bypass-blocks-present.js +9 -7
  35. package/src/checks/automatic/canvas-text-alternative-present.js +2 -0
  36. package/src/checks/automatic/combobox-name-present.js +2 -0
  37. package/src/checks/automatic/contrast-computable.js +2 -0
  38. package/src/checks/automatic/contrast-enhanced.js +2 -0
  39. package/src/checks/automatic/contrast-minimum.js +2 -0
  40. package/src/checks/automatic/css-orientation-lock.js +21 -27
  41. package/src/checks/automatic/definition-list-children-valid.js +6 -6
  42. package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
  43. package/src/checks/automatic/dialog-name-present.js +10 -10
  44. package/src/checks/automatic/dlitem-parent-valid.js +2 -0
  45. package/src/checks/automatic/duplicate-id-aria.js +4 -3
  46. package/src/checks/automatic/embed-text-alternative-present.js +2 -0
  47. package/src/checks/automatic/form-control-programmatic-label-present.js +2 -0
  48. package/src/checks/automatic/form-control-single-label.js +6 -7
  49. package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
  50. package/src/checks/automatic/iframe-focusable-content.js +246 -18
  51. package/src/checks/automatic/iframe-name-present.js +2 -0
  52. package/src/checks/automatic/iframe-title-unique.js +3 -1
  53. package/src/checks/automatic/img-alt-present.js +7 -9
  54. package/src/checks/automatic/input-image-alt-present.js +4 -6
  55. package/src/checks/automatic/label-in-name.js +15 -19
  56. package/src/checks/automatic/language-page-present.js +2 -0
  57. package/src/checks/automatic/link-in-text-block.js +2 -0
  58. package/src/checks/automatic/link-name-present.js +2 -0
  59. package/src/checks/automatic/list-children-valid.js +14 -24
  60. package/src/checks/automatic/listbox-name-present.js +2 -0
  61. package/src/checks/automatic/listitem-parent-valid.js +30 -7
  62. package/src/checks/automatic/menuitem-name-present.js +2 -0
  63. package/src/checks/automatic/meta-refresh-no-exceptions.js +4 -4
  64. package/src/checks/automatic/meta-refresh-timing-absent.js +2 -0
  65. package/src/checks/automatic/meta-viewport-zoom-enabled.js +2 -0
  66. package/src/checks/automatic/meter-name-present.js +4 -3
  67. package/src/checks/automatic/nested-interactive-controls-absent.js +27 -5
  68. package/src/checks/automatic/object-text-alternative-present.js +2 -0
  69. package/src/checks/automatic/option-name-present.js +2 -0
  70. package/src/checks/automatic/page-title-present.js +2 -0
  71. package/src/checks/automatic/progressbar-name-present.js +8 -10
  72. package/src/checks/automatic/role-img-alt-present.js +4 -4
  73. package/src/checks/automatic/searchbox-name-present.js +2 -0
  74. package/src/checks/automatic/server-side-image-map-absent.js +4 -3
  75. package/src/checks/automatic/slider-name-present.js +2 -0
  76. package/src/checks/automatic/spinbutton-name-present.js +2 -0
  77. package/src/checks/automatic/summary-name-present.js +2 -0
  78. package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
  79. package/src/checks/automatic/svg-text-alternative-present.js +17 -5
  80. package/src/checks/automatic/tab-name-present.js +2 -0
  81. package/src/checks/automatic/table-headers-attr-valid.js +3 -2
  82. package/src/checks/automatic/table-th-has-data-cells.js +2 -0
  83. package/src/checks/automatic/target-size-minimum.js +5 -0
  84. package/src/checks/automatic/td-has-header.js +24 -1
  85. package/src/checks/automatic/textbox-name-present.js +2 -0
  86. package/src/checks/automatic/tooltip-name-present.js +2 -0
  87. package/src/checks/automatic/treeitem-name-present.js +2 -0
  88. package/src/checks/automatic/valid-lang.js +2 -0
  89. package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
  90. package/src/checks/manual/accesskeys-manual.js +3 -1
  91. package/src/checks/manual/area-alt-decorative-manual.js +2 -0
  92. package/src/checks/manual/area-alt-quality-manual.js +2 -0
  93. package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
  94. package/src/checks/manual/aria-text-manual.js +6 -5
  95. package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
  96. package/src/checks/manual/css-hidden-focus.js +184 -9
  97. package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
  98. package/src/checks/manual/empty-heading-manual.js +17 -17
  99. package/src/checks/manual/empty-table-header-manual.js +52 -25
  100. package/src/checks/manual/focus-order-semantics-manual.js +16 -4
  101. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
  102. package/src/checks/manual/heading-order-manual.js +28 -1
  103. package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
  104. package/src/checks/manual/image-redundant-alt-manual.js +21 -1
  105. package/src/checks/manual/img-alt-decorative-manual.js +2 -0
  106. package/src/checks/manual/img-alt-quality-manual.js +2 -0
  107. package/src/checks/manual/input-image-alt-decorative-manual.js +2 -0
  108. package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
  109. package/src/checks/manual/label-title-only-manual.js +29 -22
  110. package/src/checks/manual/landmark-banner-is-top-level-manual.js +54 -47
  111. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +42 -26
  112. package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
  113. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
  114. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
  115. package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
  116. package/src/checks/manual/landmark-one-main-manual.js +12 -23
  117. package/src/checks/manual/landmark-unique-manual.js +37 -52
  118. package/src/checks/manual/link-name-quality-manual.js +2 -0
  119. package/src/checks/manual/media-transcript-present-manual.js +2 -0
  120. package/src/checks/manual/meta-viewport-large-manual.js +3 -1
  121. package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
  122. package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
  123. package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
  124. package/src/checks/manual/p-as-heading-manual.js +2 -0
  125. package/src/checks/manual/page-has-heading-one-manual.js +12 -11
  126. package/src/checks/manual/page-title-patterns-manual.js +2 -0
  127. package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
  128. package/src/checks/manual/region-manual.js +27 -36
  129. package/src/checks/manual/scope-attr-valid-manual.js +3 -1
  130. package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
  131. package/src/checks/manual/skip-link-manual.js +7 -6
  132. package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
  133. package/src/checks/manual/tabindex-manual.js +3 -1
  134. package/src/checks/manual/table-duplicate-name-manual.js +5 -4
  135. package/src/checks/manual/table-fake-caption-manual.js +24 -3
  136. package/src/checks/manual/video-caption-manual.js +2 -0
  137. package/src/checks/manual-review.js +2 -0
  138. package/src/core.js +6772 -2158
  139. package/src/index.js +2 -0
  140. package/src/report.js +51 -9
  141. package/src/sarif.js +18 -3
  142. package/surea11y.browser.js +2731 -999
  143. package/bin/core.js +0 -473
  144. package/docs/CLI.md +0 -128
  145. package/src/catalogs/composites.wcag.js +0 -454
  146. package/src/checks/rules-and-tags.full.csv +0 -19
  147. package/src/checks/rules-and-tags.full.json +0 -259
  148. package/src/core/aria-helpers.js +0 -1211
  149. package/src/core/contrast-helpers.js +0 -1302
  150. package/src/core/dom-helpers.js +0 -4493
  151. package/src/core/dom-runner.js +0 -787
  152. package/src/core/frame-messaging.js +0 -261
  153. package/src/core/frame-scan.js +0 -190
  154. package/src/core/rollup-composites.js +0 -127
  155. package/src/core/rule-meta.js +0 -176
  156. package/src/coverage/wcag-facets.js +0 -1079
  157. package/src/coverage/wcag-version-map.js +0 -84
  158. package/src/i18n/en.js +0 -1228
  159. package/src/i18n/fr.js +0 -1185
  160. package/src/policy/contracts.js +0 -18
  161. package/src/policy/resolvePolicy.js +0 -59
  162. package/src/policy/schemas/engine-options.schema.json +0 -103
  163. 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
  /**
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -13,25 +15,26 @@
13
15
  * @expectation
14
16
  * Each attribute's value conforms to its WAI-ARIA-declared value type:
15
17
  * boolean ("true"/"false"), tristate ("true"/"false"/"mixed"), a token
16
- * from a fixed enumerated set, an integer, a real number, or a non-empty
17
- * ID reference (list) that resolves to an existing element in the
18
- * document.
18
+ * from a fixed enumerated set, an integer, a real number, or an empty
19
+ * value or ID reference (list) that resolves to an existing element in
20
+ * the document.
19
21
  * @implementation-notes
20
22
  * - Not rule-gated on isAccTreeEligible: this remains a static-markup
21
23
  * property, while engine-level hidden-subtree filtering still applies
22
24
  * unless engineOptions.includeHiddenElements is true.
23
- * - ID-reference resolution (added 2026-07-20, see aria-helpers.js's
24
- * idExists) only flags idref-list attributes (aria-labelledby,
25
- * aria-describedby, aria-controls, aria-owns, etc.) when NONE of the
26
- * space-separated ids resolve — a partially-dangling list (some ids
27
- * exist, some don't) is left unflagged. Verified 2026-07-21 directly
28
- * against a widely-used reference engine's own `validateAttrValue` source:
29
- * this is not a conservative guess, it's an exact match for that engine's
30
- * own behavior (`idrefs(vNode, attr).some(node => !!node)` — that engine
31
- * itself only invalidates when every token fails to resolve). Single-idref
32
- * attributes (aria-activedescendant, aria-errormessage) are flagged
33
- * whenever their one id doesn't resolve, also matching that engine's `idref`
34
- * case exactly.
25
+ * - ID-reference resolution (see aria-helpers.js's idExists) only flags
26
+ * idref-list attributes (aria-labelledby, aria-describedby,
27
+ * aria-controls, aria-owns, etc.) when NONE of the space-separated ids
28
+ * resolve — a partially-dangling list (some ids exist, some don't) is
29
+ * left unflagged (only invalidate when every token fails to resolve).
30
+ * Single-idref attributes (aria-activedescendant, aria-errormessage) are
31
+ * flagged whenever their one id doesn't resolve.
32
+ * - An explicitly-EMPTY idref/idref-list value (e.g.
33
+ * `aria-describedby=""`) is valid, not a violation — `allowEmpty` holds
34
+ * for every idref/idref-list ARIA attribute. A common, deliberate pattern
35
+ * in templated markup (e.g. React conditionally rendering
36
+ * `aria-describedby={hasError ? errorId : ''}`); flagging it would be a
37
+ * false positive.
35
38
  */
36
39
 
37
40
  const id = 'aria-valid-attr-value';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -19,8 +21,7 @@
19
21
  * @implementation-notes
20
22
  * - Static text-based parsing of the style attribute string (regex for
21
23
  * the three property names followed by `!important`), not computed
22
- * style — matches the property list a widely-used reference engine's avoid-inline-spacing
23
- * rule checks.
24
+ * style.
24
25
  */
25
26
 
26
27
  const id = 'avoid-inline-spacing';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'binary-control-name-present';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  // NOTE: Repo ruleId contract requires ENGINE_TAG prefix in the rule id.
@@ -47,9 +49,8 @@ function runInPage(ctx) {
47
49
  // own accessible name (img alt, aria-label/aria-labelledby, title) when
48
50
  // it has one, not just literal text nodes. See getContentNameInfo's
49
51
  // header comment in src/core/dom-helpers.js for the full rationale
50
- // (this replaced a text-node-only TreeWalker that missed the common
51
- // "<a><img alt='...'></a>" logo-link / "<button><img alt='...'></button>"
52
- // icon-button pattern).
52
+ // (covers the common "<a><img alt='...'></a>" logo-link /
53
+ // "<button><img alt='...'></button>" icon-button pattern).
53
54
  if (helpers.getContentNameInfo) {
54
55
  const info = helpers.getContentNameInfo(container, ctx);
55
56
  return info && info.present ? info.value : '';
@@ -107,10 +108,9 @@ function runInPage(ctx) {
107
108
  // one of these roles is no longer semantically a button — per the WAI-ARIA
108
109
  // Accessible Name and Description Computation spec these roles are
109
110
  // name-from-author-only, and their rendered content represents a VALUE,
110
- // not a NAME. Found on a real page (Spotify's "sort by" control): a
111
- // <button role="combobox">List</button> where "List" is the combobox's
112
- // currently selected value, not a label for what the combobox is —
113
- // crediting it as the accessible name masked a real missing-name bug.
111
+ // not a NAME. E.g. <button role="combobox">List</button> where "List" is
112
+ // the combobox's currently selected value, not a label — crediting it as
113
+ // the accessible name would mask a missing name.
114
114
  const VALUE_ROLES = [
115
115
  'textbox',
116
116
  'progressbar',
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -27,8 +29,8 @@
27
29
  * mechanism (e.g. a screen reader's "jump by heading" command).
28
30
  * @implementation-notes
29
31
  * - This rule intentionally checks presence, not position, for the
30
- * same-page-anchor condition (b): a widely-used reference engine's real bypass algorithm is
31
- * heuristic (see ROADMAP.md's Tier 1a note on why this rule was
32
+ * same-page-anchor condition (b): a full bypass algorithm is heuristic
33
+ * (see ROADMAP.md's Tier 1a note on why this rule was
32
34
  * deferred from the rest of that batch), and getting DOM-order /
33
35
  * keyboard-focus-order positioning exactly right without introducing
34
36
  * false positives is materially harder than the rest of Tier 1a. Being
@@ -115,12 +117,12 @@ function runInPage(ctx) {
115
117
  }
116
118
 
117
119
  // Filters through isAccTreeEligible (hidden/aria-hidden/display:none/inert
118
- // don't count as "a bypass mechanism is present") -- see page-has-heading-
119
- // one-manual.js's identical fix for the real-world trigger (CDC's flu
120
- // page, 2026-07-30: its only <h1> sits inside a display:none ancestor,
120
+ // don't count as "a bypass mechanism is present") -- see
121
+ // page-has-heading-one-manual.js for the identical real-world trigger
122
+ // (e.g. a page whose only <h1> sits inside a display:none ancestor,
121
123
  // unreachable by sighted and screen reader users alike). A fully
122
- // non-rendered <main>/heading was previously credited here too, wrongly
123
- // returning `pass` for a page with zero actual bypass mechanisms.
124
+ // non-rendered <main>/heading must not be credited here, since that would
125
+ // wrongly return `pass` for a page with zero actual bypass mechanisms.
124
126
  function hasMainLandmark() {
125
127
  for (const el of queryAll('main, [role="main"]')) {
126
128
  if (el && isExposedToAt(el)) return true;
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'combobox-name-present';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'contrast-computable';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'contrast-enhanced';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'contrast-minimum';
@@ -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
@@ -98,9 +92,9 @@ function runInPage(ctx) {
98
92
  }
99
93
 
100
94
  // Converts a rotate()/rotateZ() angle argument (with unit) to degrees.
101
- // Matches a widely-used reference engine's own getAngleInDegrees: only deg/grad/rad/turn are
102
- // recognized; a unitless or unrecognized value contributes 0 (ignored,
103
- // 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).
104
98
  function angleToDegrees(raw) {
105
99
  const m = /(-?[\d.]+)\s*(deg|grad|rad|turn)/i.exec(raw);
106
100
  if (!m) return 0;
@@ -127,17 +121,17 @@ function runInPage(ctx) {
127
121
  return total;
128
122
  }
129
123
 
130
- // Whole-page-orientation-lock detection, matching a widely-used reference engine's own degree
131
- // math exactly (see @implementation-notes): a rotation near 0 or 180
132
- // degrees (mod 180) is a no-op or a flip, neither of which changes
133
- // portrait<->landscape, so it's explicitly NOT a lock; only a rotation
134
- // 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.
135
129
  function isLockingRotation(styleDecl) {
136
130
  if (!styleDecl) return false;
137
131
  const transformVal =
138
132
  trim(styleDecl.transform) ||
139
133
  trim(styleDecl.getPropertyValue ? styleDecl.getPropertyValue('-webkit-transform') : '');
140
- // The standalone CSS `rotate` property (distinct from `transform: rotate()`) -- that reference engine checks this too.
134
+ // The standalone CSS `rotate` property (distinct from `transform: rotate()`).
141
135
  const rotatePropVal = trim(
142
136
  styleDecl.getPropertyValue ? styleDecl.getPropertyValue('rotate') : ''
143
137
  );
@@ -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.
@@ -108,8 +109,7 @@ function runInPage(ctx) {
108
109
  }
109
110
  const dedupedInvalidTags = [...new Set(invalidTags)];
110
111
 
111
- // Verified 2026-07-20 against a widely-used reference engine's definition-list check:
112
- // 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
113
113
  // NEITHER dt nor dd (whether genuinely childless after flattening, only
114
114
  // passthrough script/template/style content, or an empty wrapping div)
115
115
  // is vacuously fine, not a violation. Only an UNBALANCED pairing (dt
@@ -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
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'dialog-name-present';
@@ -55,16 +57,14 @@ function runInPage(ctx) {
55
57
  // Resolve via the shared getTextFromIdRefs helper — computes each
56
58
  // referenced element's own ACCESSIBLE NAME (aria-label, then
57
59
  // aria-labelledby, then a value-like name, then content, then title),
58
- // not just its content text. Found via a real page (BBC News' cookie-
59
- // consent dialog, 2026-07-22): aria-labelledby pointed at an
60
- // <iframe title="SP Consent Message">, whose only name source is its
61
- // title attribute (an iframe's content is opaque/cross-origin per
62
- // HTML-AAM, so "name from content" is always empty). The previous
63
- // version here only ever computed name-from-content of the referenced
64
- // node (via getConservativeSubtreeText), silently missing the title
65
- // fallback and reporting no accessible name at all — the identical
66
- // pattern was hand-copied into 15 other *-name-present rules; all
67
- // 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.
68
68
  if (helpers.getTextFromIdRefs) {
69
69
  try {
70
70
  const r = helpers.getTextFromIdRefs(raw, ctx, { maxRefs: maxRefs || 8 });
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -18,9 +20,8 @@
18
20
  * which element the reference resolves to (typically the first, silently
19
21
  * dropping the others).
20
22
  * @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).
23
+ * - Scoped deliberately to ids referenced by ARIA, not the broader/
24
+ * deprecated page-wide duplicate-id check (see ROADMAP.md's "Skip" list).
24
25
  * - Document-wide by design: id uniqueness and ARIA id references are a
25
26
  * whole-document property, not scoped to a sub-root.
26
27
  */
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -17,16 +19,13 @@
17
19
  * are ambiguous: many screen readers only announce one of them, and it
18
20
  * is not deterministic which.
19
21
  * @implementation-notes
20
- * - Matches a widely-used reference engine's form-field-multiple-labels. Distinct, atomic
21
- * decision from form-control-programmatic-label-present (that
22
- * rule checks a label exists at all; this one checks there is at most
23
- * one).
22
+ * - Distinct, atomic decision from form-control-programmatic-label-present
23
+ * (that rule checks a label exists at all; this one checks there is at
24
+ * most one).
24
25
  * - A label that isn't accessibility-tree-eligible (display:none,
25
26
  * aria-hidden, etc.) is excluded before counting: it can't contribute to
26
27
  * the control's accessible name, so it can't create the ambiguity this
27
- * rule exists to catch. Matches the reference engine's own
28
- * multiple-label check, which filters hidden labels the same way before
29
- * deciding whether there's more than one.
28
+ * rule exists to catch.
30
29
  */
31
30
 
32
31
  const id = 'form-control-single-label';
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**