@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
  const id = 'label-in-name';
@@ -5,7 +7,7 @@ const id = 'label-in-name';
5
7
  const meta = {
6
8
  title: 'Label in Name: accessible name contains visible text',
7
9
  description:
8
- 'Checks that when a control has a visible text label, the accessible name (from aria-label/aria-labelledby) contains that visible label text (WCAG 2.5.3).',
10
+ 'Checks that when a control has a visible text label, the accessible name (from aria-label/aria-labelledby) contains that visible label text (WCAG 2.5.3).',
9
11
  i18n: {
10
12
  titleKey: 'labelInName_title',
11
13
  descriptionKey: 'labelInName_description'
@@ -37,20 +39,27 @@ function runInPage(ctx) {
37
39
 
38
40
  // Applicability: focus/activation controls with explicit ARIA naming.
39
41
  // NOTE: aria-hidden is intentionally NOT excluded here; it does not affect visual rendering.
40
- const selector = ':is(button, a[href], summary, input:not([type="hidden"]), textarea, select, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="searchbox"], [role="tab"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], [role="treeitem"], [role="gridcell"]):not([hidden]):not([disabled]):not([aria-disabled="true"]):is([aria-label], [aria-labelledby])';
42
+ const selector =
43
+ ':is(button, a[href], summary, input:not([type="hidden"]), textarea, select, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="searchbox"], [role="tab"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], [role="treeitem"], [role="gridcell"]):not([hidden]):not([disabled]):not([aria-disabled="true"]):is([aria-label], [aria-labelledby])';
41
44
 
42
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
45
+ const nodes = helpers.queryAllSmart
46
+ ? helpers.queryAllSmart(selector)
47
+ : helpers.queryAll(selector);
43
48
 
44
49
  function norm(s) {
45
- const v = (s == null ? '' : String(s));
50
+ const v = s == null ? '' : String(s);
46
51
  // deterministic normalization: trim + collapse whitespace + case-fold
47
52
  return v.replace(/\s+/g, ' ').trim().toLowerCase();
48
53
  }
49
54
 
50
55
  function getElementDescriptor(el) {
51
- const tag = (el && el.tagName ? String(el.tagName).toLowerCase() : 'element');
52
- let role = '';
53
- try { role = el && el.getAttribute ? (el.getAttribute('role') || '') : ''; } catch { role = ''; }
56
+ const tag = el && el.tagName ? String(el.tagName).toLowerCase() : 'element';
57
+ let role;
58
+ try {
59
+ role = el && el.getAttribute ? el.getAttribute('role') || '' : '';
60
+ } catch {
61
+ role = '';
62
+ }
54
63
  const r = String(role || '').trim();
55
64
  return r ? `${tag}[role="${r}"]` : tag;
56
65
  }
@@ -62,35 +71,38 @@ function runInPage(ctx) {
62
71
  }
63
72
 
64
73
  function isNonRenderedTag(el) {
65
- const tn = (el && el.tagName ? String(el.tagName).toLowerCase() : '');
66
- return tn === 'script' || tn === 'style' || tn === 'template' || tn === 'noscript' || tn === 'meta' || tn === 'link';
74
+ const tn = el && el.tagName ? String(el.tagName).toLowerCase() : '';
75
+ return (
76
+ tn === 'script' ||
77
+ tn === 'style' ||
78
+ tn === 'template' ||
79
+ tn === 'noscript' ||
80
+ tn === 'meta' ||
81
+ tn === 'link'
82
+ );
67
83
  }
68
84
 
69
85
  function isDomVisible(el) {
70
86
  if (!el) return false;
71
- if (helpers.isDomVisibleEligible) return !!helpers.isDomVisibleEligible(el, ctx, { targetSet: 'dom' }).eligible;
72
- if (helpers.getEligibilityInfo) return !!helpers.getEligibilityInfo(el, ctx, { targetSet: 'dom' }).eligible;
87
+ if (helpers.isDomVisibleEligible)
88
+ return !!helpers.isDomVisibleEligible(el, ctx, { targetSet: 'dom' }).eligible;
89
+ if (helpers.getEligibilityInfo)
90
+ return !!helpers.getEligibilityInfo(el, ctx, { targetSet: 'dom' }).eligible;
73
91
  return true;
74
92
  }
75
93
 
76
94
  // Unlike isDomVisible above, this also excludes aria-hidden subtrees.
77
- // Needed specifically for collectVisibleTextUnder's per-text-node check
78
- // below: an aria-hidden icon-font glyph (e.g. Material Icons' ligature
79
- // pattern, <mat-icon aria-hidden="true">format_color_fill</mat-icon>) is
80
- // technically DOM-visible pixels, but is never perceived by a sighted or
81
- // voice-control user as literal readable words the way real visible text
82
- // is — a widely-used reference engine's own label-content-name-mismatch check reaches the same
83
- // practical outcome via a canvas-based ligature-detection heuristic
84
- // (measuring rendered glyph shapes), not replicable here since this rule
85
- // runs against static markup with no real canvas/font rendering
86
- // available; excluding aria-hidden content is a cheaper, static-markup
87
- // signal that gets the common case (decorative icon fonts) right without
88
- // needing one. Confirmed via a live-DOM cross-engine run 2026-07-21
89
- // (Angular Material's theme-picker button, aria-label="Select a theme",
90
- // aria-hidden icon rendering literally as "format_color_fill").
95
+ // Needed for collectVisibleTextUnder's per-text-node check below: an
96
+ // aria-hidden icon-font glyph (e.g. an <i aria-hidden="true"> ligature
97
+ // rendering as "format_color_fill") is DOM-visible pixels but is never
98
+ // perceived as literal readable words the way real visible text is.
99
+ // Excluding aria-hidden content is a cheap static-markup signal that gets
100
+ // the common case (decorative icon fonts) right — an icon-only button
101
+ // named via aria-label shouldn't have its glyph name counted as text.
91
102
  function isAccEligible(el) {
92
103
  if (!el) return false;
93
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
104
+ const fn =
105
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
94
106
  if (!fn) return isDomVisible(el);
95
107
  try {
96
108
  const r = fn(el, ctx);
@@ -105,11 +117,12 @@ function runInPage(ctx) {
105
117
  // than referencing the global NodeFilter object directly: runInPage must
106
118
  // have zero free vars (see docs/RULE_AUTHORING.md's free-var footgun) and
107
119
  // NodeFilter is not itself present in the execution realm this function
108
- // actually runs in, unlike window/document. Confirmed 2026-07-21 — this
109
- // silently made createTreeWalker throw on every call, falling back to
110
- // raw container.textContent (which respects none of isNonRenderedTag/
111
- // isDomVisible/isAccEligible below, since that whole per-node loop is
112
- // skipped in the fallback path). Same pattern already used correctly in
120
+ // actually runs in, unlike window/document. Referencing the global
121
+ // directly would silently make createTreeWalker throw on every call,
122
+ // falling back to raw container.textContent (which respects none of
123
+ // isNonRenderedTag/isDomVisible/isAccEligible below, since that whole
124
+ // per-node loop is skipped in the fallback path). Same pattern already
125
+ // used correctly in
113
126
  // region-manual.js's own createTreeWalker call.
114
127
  const SHOW_TEXT = 4;
115
128
 
@@ -118,7 +131,7 @@ function runInPage(ctx) {
118
131
  if (!isDomVisible(container)) return '';
119
132
 
120
133
  // TreeWalker is deterministic in document order.
121
- let walker = null;
134
+ let walker;
122
135
  try {
123
136
  walker = document.createTreeWalker(container, SHOW_TEXT, null);
124
137
  } catch {
@@ -134,7 +147,7 @@ function runInPage(ctx) {
134
147
  }
135
148
 
136
149
  const parts = [];
137
- let n = null;
150
+ let n;
138
151
  // eslint-disable-next-line no-cond-assign
139
152
  while ((n = walker.nextNode())) {
140
153
  try {
@@ -183,7 +196,7 @@ function runInPage(ctx) {
183
196
  // contextSelector support), not a single element with its own
184
197
  // .querySelector to call directly.
185
198
  try {
186
- const idAttribute = control && control.getAttribute ? (control.getAttribute('id') || '') : '';
199
+ const idAttribute = control && control.getAttribute ? control.getAttribute('id') || '' : '';
187
200
  const key = String(idAttribute || '').trim();
188
201
  if (key && document && document.querySelector) {
189
202
  const l = document.querySelector('label[for="' + CSS.escape(key) + '"]');
@@ -215,10 +228,10 @@ function runInPage(ctx) {
215
228
  // 1) <label> association for form controls
216
229
  // 2) visible text inside the element
217
230
  // 3) visible text from aria-labelledby referenced elements
218
- let text = '';
231
+ let text;
219
232
  let source = 'none';
220
233
 
221
- const tn = (el && el.tagName ? String(el.tagName).toLowerCase() : '');
234
+ const tn = el && el.tagName ? String(el.tagName).toLowerCase() : '';
222
235
 
223
236
  // 1) Label association (native form controls)
224
237
  const isFormControl = tn === 'input' || tn === 'select' || tn === 'textarea';
@@ -244,7 +257,7 @@ function runInPage(ctx) {
244
257
  if (idrefs && helpers.resolveIdRefs) {
245
258
  const r = helpers.resolveIdRefs(idrefs, ctx, { maxRefs: 8 });
246
259
  const parts = [];
247
- for (const ref of (r && Array.isArray(r.refs) ? r.refs : [])) {
260
+ for (const ref of r && Array.isArray(r.refs) ? r.refs : []) {
248
261
  if (!ref || !ref.tagName) continue;
249
262
  if (!isDomVisible(ref)) continue;
250
263
  const t = collectVisibleTextUnder(ref);
@@ -275,7 +288,9 @@ function runInPage(ctx) {
275
288
 
276
289
  let acc = { present: false, value: '', mechanism: 'none', flags: [] };
277
290
  try {
278
- acc = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx, { maxRefs: 8 }) : acc;
291
+ acc = helpers.getAccessibleNameInfo
292
+ ? helpers.getAccessibleNameInfo(el, ctx, { maxRefs: 8 })
293
+ : acc;
279
294
  } catch {
280
295
  acc = { present: false, value: '', mechanism: 'none', flags: ['exception'] };
281
296
  }
@@ -286,7 +301,9 @@ function runInPage(ctx) {
286
301
 
287
302
  if (!contains) {
288
303
  const selectorOut = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
289
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
304
+ const html = helpers.getOuterHtmlSnippet
305
+ ? helpers.getOuterHtmlSnippet(el)
306
+ : el.outerHTML || '';
290
307
 
291
308
  occurrences.push({
292
309
  selector: selectorOut,
@@ -317,8 +334,15 @@ function runInPage(ctx) {
317
334
  }
318
335
  }
319
336
 
320
- if (applicableCount === 0) return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
321
- if (occurrences.length) return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
337
+ if (applicableCount === 0)
338
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
339
+ if (occurrences.length)
340
+ return {
341
+ ruleId: rule.ruleId,
342
+ outcome: 'fail',
343
+ severity: rule.defaultSeverity || 'minor',
344
+ occurrences
345
+ };
322
346
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
323
347
  }
324
348
 
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -34,33 +36,33 @@
34
36
  const id = 'html-lang-attr-present';
35
37
 
36
38
  const meta = {
37
- title: 'Page language is declared',
38
- description: 'Checks that the default language of the page is programmatically declared.',
39
- i18n: {
40
- titleKey: 'html_lang_attr_title',
41
- descriptionKey: 'html_lang_attr_description'
42
- },
43
- helpUrl: null,
44
- tags: ['wcag2a', 'wcag311', 'structure', 'language', 'automatic', 'atomic'],
45
- wcagSc: ['3.1.1'],
46
- normativeMappings: [
47
- {
48
- standard: 'WCAG',
49
- version: '2.2',
50
- requirement: '3.1.1',
51
- title: 'Language of Page',
52
- conformanceLevel: 'A'
53
- }
54
- ],
55
- defaultSeverity: 'serious',
56
- category: 'understandable',
57
- type: 'automatic',
58
- defaultConfidence: 'high',
59
- coverage: {
60
- facetsBySc: {
61
- '3.1.1': ['html-lang-attr-present']
62
- }
39
+ title: 'Page language is declared',
40
+ description: 'Checks that the default language of the page is programmatically declared.',
41
+ i18n: {
42
+ titleKey: 'html_lang_attr_title',
43
+ descriptionKey: 'html_lang_attr_description'
44
+ },
45
+ helpUrl: null,
46
+ tags: ['wcag2a', 'wcag311', 'structure', 'language', 'automatic', 'atomic'],
47
+ wcagSc: ['3.1.1'],
48
+ normativeMappings: [
49
+ {
50
+ standard: 'WCAG',
51
+ version: '2.2',
52
+ requirement: '3.1.1',
53
+ title: 'Language of Page',
54
+ conformanceLevel: 'A'
55
+ }
56
+ ],
57
+ defaultSeverity: 'serious',
58
+ category: 'understandable',
59
+ type: 'automatic',
60
+ defaultConfidence: 'high',
61
+ coverage: {
62
+ facetsBySc: {
63
+ '3.1.1': ['html-lang-attr-present']
63
64
  }
65
+ }
64
66
  };
65
67
 
66
68
  // This check is inherently whole-document (does the PAGE have this
@@ -72,96 +74,96 @@ function applicability(ctx) {
72
74
  }
73
75
 
74
76
  function runInPage(ctx) {
75
- const { document, rule, helpers } = ctx;
76
- const html = document && document.documentElement;
77
-
78
- const tag = (html && html.tagName) ? String(html.tagName).toLowerCase() : '';
79
- if (!html || tag !== 'html') {
80
- return {
81
- ruleId: rule.ruleId,
82
- outcome: 'notApplicable',
83
- severity: 'minor',
84
- occurrences: []
85
- };
86
- }
77
+ const { document, rule, helpers } = ctx;
78
+ const html = document && document.documentElement;
87
79
 
88
- const visibilityFilter = { targetSet: 'acc', accEligible: true, reasons: [] };
89
-
90
- function pushFail(summaryKey, hintKey, params, details) {
91
- const baseOccurrence = {
92
- summary: '',
93
- hint: '',
94
- i18n: {
95
- summaryKey,
96
- hintKey,
97
- params: params && typeof params === 'object' ? params : {}
98
- },
99
- data: {
100
- visibilityFilter,
101
- details: details && typeof details === 'object' ? details : {}
102
- }
103
- };
104
-
105
- if (helpers && typeof helpers.reportOccurrence === 'function') {
106
- return [helpers.reportOccurrence(html, baseOccurrence)];
107
- }
108
-
109
- // Never compute selector/snippet in the rule.
110
- return [{ selector: '', html: '', ...baseOccurrence }];
111
- }
80
+ const tag = html && html.tagName ? String(html.tagName).toLowerCase() : '';
81
+ if (!html || tag !== 'html') {
82
+ return {
83
+ ruleId: rule.ruleId,
84
+ outcome: 'notApplicable',
85
+ severity: 'minor',
86
+ occurrences: []
87
+ };
88
+ }
89
+
90
+ const visibilityFilter = { targetSet: 'acc', accEligible: true, reasons: [] };
91
+
92
+ function pushFail(summaryKey, hintKey, params, details) {
93
+ const baseOccurrence = {
94
+ summary: '',
95
+ hint: '',
96
+ i18n: {
97
+ summaryKey,
98
+ hintKey,
99
+ params: params && typeof params === 'object' ? params : {}
100
+ },
101
+ data: {
102
+ visibilityFilter,
103
+ details: details && typeof details === 'object' ? details : {}
104
+ }
105
+ };
112
106
 
113
- const rawLang = html.getAttribute('lang'); // null if missing
114
- const lang = (rawLang || '').trim();
115
-
116
- if (rawLang === null) {
117
- return {
118
- ruleId: rule.ruleId,
119
- outcome: 'fail',
120
- severity: rule.defaultSeverity,
121
- occurrences: pushFail(
122
- 'html_lang_attr_missing_absent',
123
- 'html_lang_attr_hint_missing_absent',
124
- {},
125
- { reasonCode: 'lang-missing', location: 'html' }
126
- )
127
- };
107
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
108
+ return [helpers.reportOccurrence(html, baseOccurrence)];
128
109
  }
129
110
 
130
- if (lang.length === 0) {
131
- return {
132
- ruleId: rule.ruleId,
133
- outcome: 'fail',
134
- severity: rule.defaultSeverity,
135
- occurrences: pushFail(
136
- 'html_lang_attr_missing_empty',
137
- 'html_lang_attr_hint_missing_empty',
138
- {},
139
- { reasonCode: 'lang-empty', location: 'html' }
140
- )
141
- };
142
- }
111
+ // Never compute selector/snippet in the rule.
112
+ return [{ selector: '', html: '', ...baseOccurrence }];
113
+ }
143
114
 
144
- // Minimal BCP47 primary subtag check
145
- if (!/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(lang)) {
146
- return {
147
- ruleId: rule.ruleId,
148
- outcome: 'fail',
149
- severity: rule.defaultSeverity,
150
- occurrences: pushFail(
151
- 'html_lang_attr_invalid',
152
- 'html_lang_attr_hint_invalid',
153
- { lang },
154
- { reasonCode: 'lang-invalid-bcp47', lang }
155
- )
156
- };
157
- }
115
+ const rawLang = html.getAttribute('lang'); // null if missing
116
+ const lang = (rawLang || '').trim();
117
+
118
+ if (rawLang === null) {
119
+ return {
120
+ ruleId: rule.ruleId,
121
+ outcome: 'fail',
122
+ severity: rule.defaultSeverity,
123
+ occurrences: pushFail(
124
+ 'html_lang_attr_missing_absent',
125
+ 'html_lang_attr_hint_missing_absent',
126
+ {},
127
+ { reasonCode: 'lang-missing', location: 'html' }
128
+ )
129
+ };
130
+ }
131
+
132
+ if (lang.length === 0) {
133
+ return {
134
+ ruleId: rule.ruleId,
135
+ outcome: 'fail',
136
+ severity: rule.defaultSeverity,
137
+ occurrences: pushFail(
138
+ 'html_lang_attr_missing_empty',
139
+ 'html_lang_attr_hint_missing_empty',
140
+ {},
141
+ { reasonCode: 'lang-empty', location: 'html' }
142
+ )
143
+ };
144
+ }
158
145
 
146
+ // Minimal BCP47 primary subtag check
147
+ if (!/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(lang)) {
159
148
  return {
160
- ruleId: rule.ruleId,
161
- outcome: 'pass',
162
- severity: 'minor',
163
- occurrences: []
149
+ ruleId: rule.ruleId,
150
+ outcome: 'fail',
151
+ severity: rule.defaultSeverity,
152
+ occurrences: pushFail(
153
+ 'html_lang_attr_invalid',
154
+ 'html_lang_attr_hint_invalid',
155
+ { lang },
156
+ { reasonCode: 'lang-invalid-bcp47', lang }
157
+ )
164
158
  };
159
+ }
160
+
161
+ return {
162
+ ruleId: rule.ruleId,
163
+ outcome: 'pass',
164
+ severity: 'minor',
165
+ occurrences: []
166
+ };
165
167
  }
166
168
 
167
- module.exports = { id, meta, runInPage, applicability };
169
+ 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
  /**
@@ -47,7 +49,8 @@
47
49
  const id = 'link-in-text-block';
48
50
 
49
51
  const meta = {
50
- title: 'Links in text blocks must be distinguishable from surrounding text without relying on color alone',
52
+ title:
53
+ 'Links in text blocks must be distinguishable from surrounding text without relying on color alone',
51
54
  description:
52
55
  'Checks that a link inside a run of text is visually distinguishable from the surrounding text by underline, a font-weight/style difference, or a sufficient (>=3:1) color-contrast difference — not by color alone.',
53
56
  i18n: {
@@ -55,10 +58,26 @@ const meta = {
55
58
  descriptionKey: 'linkInTextBlock_description'
56
59
  },
57
60
  helpUrl: null,
58
- tags: ['wcag2a', 'wcag141', 'navigation', 'color', 'links', 'contrast', 'atomic', 'automatic', 'dom'],
61
+ tags: [
62
+ 'wcag2a',
63
+ 'wcag141',
64
+ 'navigation',
65
+ 'color',
66
+ 'links',
67
+ 'contrast',
68
+ 'atomic',
69
+ 'automatic',
70
+ 'dom'
71
+ ],
59
72
  wcagSc: ['1.4.1'],
60
73
  normativeMappings: [
61
- { standard: 'WCAG', version: '2.2', requirement: '1.4.1', title: 'Use of Color', conformanceLevel: 'A' }
74
+ {
75
+ standard: 'WCAG',
76
+ version: '2.2',
77
+ requirement: '1.4.1',
78
+ title: 'Use of Color',
79
+ conformanceLevel: 'A'
80
+ }
62
81
  ],
63
82
  defaultSeverity: 'serious',
64
83
  category: 'perceivable',
@@ -68,7 +87,7 @@ const meta = {
68
87
  };
69
88
 
70
89
  function runInPage(ctx) {
71
- const { document, helpers, rule, engineOptions } = ctx;
90
+ const { helpers, rule, engineOptions } = ctx;
72
91
 
73
92
  function safeComputedStyle(el) {
74
93
  try {
@@ -77,14 +96,16 @@ function runInPage(ctx) {
77
96
  const cs = helpers.computedStyle(el);
78
97
  if (cs) return cs;
79
98
  }
80
- const view = (el.ownerDocument && el.ownerDocument.defaultView) ? el.ownerDocument.defaultView : null;
99
+ const view =
100
+ el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : null;
81
101
  if (view && typeof view.getComputedStyle === 'function') return view.getComputedStyle(el);
82
102
  } catch {}
83
103
  return null;
84
104
  }
85
105
 
86
106
  function decorationTokens(cs) {
87
- const raw = `${(cs && cs.textDecorationLine) || ''} ${(cs && cs.textDecoration) || ''}`.toLowerCase();
107
+ const raw =
108
+ `${(cs && cs.textDecorationLine) || ''} ${(cs && cs.textDecoration) || ''}`.toLowerCase();
88
109
  return raw.split(/\s+/).filter(Boolean);
89
110
  }
90
111
 
@@ -102,9 +123,10 @@ function runInPage(ctx) {
102
123
  return false;
103
124
  }
104
125
 
105
- const contrastOpts = engineOptions && typeof engineOptions.contrast === 'object' && engineOptions.contrast
106
- ? engineOptions.contrast
107
- : {};
126
+ const contrastOpts =
127
+ engineOptions && typeof engineOptions.contrast === 'object' && engineOptions.contrast
128
+ ? engineOptions.contrast
129
+ : {};
108
130
  const mode = contrastOpts.mode === 'auditorAssist' ? 'auditorAssist' : 'strictConformance';
109
131
  const rootCanvasFallback =
110
132
  typeof contrastOpts.rootCanvasFallback === 'string' && contrastOpts.rootCanvasFallback.trim()
@@ -114,7 +136,9 @@ function runInPage(ctx) {
114
136
  const c = helpers && helpers.contrast ? helpers.contrast : null;
115
137
 
116
138
  const selector = 'a[href]';
117
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
139
+ const nodes = helpers.queryAllSmart
140
+ ? helpers.queryAllSmart(selector)
141
+ : helpers.queryAll(selector);
118
142
 
119
143
  const occurrences = [];
120
144
  let applicableCount = 0;
@@ -123,7 +147,8 @@ function runInPage(ctx) {
123
147
  if (!el || !el.getAttribute) continue;
124
148
 
125
149
  const eligResult = helpers.isAccTreeEligible ? helpers.isAccTreeEligible(el, ctx) : true;
126
- const eligible = typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
150
+ const eligible =
151
+ typeof eligResult === 'boolean' ? eligResult : !!(eligResult && eligResult.eligible);
127
152
  if (!eligible) continue;
128
153
 
129
154
  const parent = el.parentElement;
@@ -156,13 +181,22 @@ function runInPage(ctx) {
156
181
  if (blocker && blocker.ok === false) {
157
182
  // Not confidently computable — skip (benefit of the doubt).
158
183
  } else {
159
- const bg = c.computeEffectiveBackground(el, { contrast: { mode, rootCanvasFallback }, collectStack: false });
184
+ const bg = c.computeEffectiveBackground(el, {
185
+ contrast: { mode, rootCanvasFallback },
186
+ collectStack: false
187
+ });
160
188
  const fgLink = c.computeEffectiveForeground(el);
161
189
  const fgParent = c.computeEffectiveForeground(parent);
162
190
 
163
191
  if (bg && bg.ok && bg.rgba && fgLink && fgLink.rgba && fgParent && fgParent.rgba) {
164
- const fgLinkOpaque = fgLink.rgba.a < 1 ? c.compositeRgba(fgLink.rgba, bg.rgba) : { r: fgLink.rgba.r, g: fgLink.rgba.g, b: fgLink.rgba.b, a: 1 };
165
- const fgParentOpaque = fgParent.rgba.a < 1 ? c.compositeRgba(fgParent.rgba, bg.rgba) : { r: fgParent.rgba.r, g: fgParent.rgba.g, b: fgParent.rgba.b, a: 1 };
192
+ const fgLinkOpaque =
193
+ fgLink.rgba.a < 1
194
+ ? c.compositeRgba(fgLink.rgba, bg.rgba)
195
+ : { r: fgLink.rgba.r, g: fgLink.rgba.g, b: fgLink.rgba.b, a: 1 };
196
+ const fgParentOpaque =
197
+ fgParent.rgba.a < 1
198
+ ? c.compositeRgba(fgParent.rgba, bg.rgba)
199
+ : { r: fgParent.rgba.r, g: fgParent.rgba.g, b: fgParent.rgba.b, a: 1 };
166
200
 
167
201
  ratio = c.contrastRatio(fgLinkOpaque, fgParentOpaque);
168
202
  fgLinkHex = c.rgbToHex ? c.rgbToHex(fgLinkOpaque) : '';
@@ -178,16 +212,19 @@ function runInPage(ctx) {
178
212
 
179
213
  if (!flagged) continue;
180
214
 
181
- const eligInfo = helpers.getEligibilityInfo ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
215
+ const eligInfo = helpers.getEligibilityInfo
216
+ ? helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' })
217
+ : null;
182
218
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
183
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
219
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
184
220
  const tag = (el.tagName || '').toLowerCase();
185
221
  const ratioStr = c.round2 ? c.round2(ratio) : String(ratio);
186
222
 
187
223
  occurrences.push({
188
224
  selector: stableSelector,
189
225
  html,
190
- summary: 'This link in a block of text relies on color alone to be distinguished from the surrounding text.',
226
+ summary:
227
+ 'This link in a block of text relies on color alone to be distinguished from the surrounding text.',
191
228
  hint: 'Add an underline, a font-weight/style difference, or increase the color contrast between the link and surrounding text to at least 3:1.',
192
229
  i18n: {
193
230
  summaryKey: 'linkInTextBlock_summary_fail',
@@ -209,9 +246,14 @@ function runInPage(ctx) {
209
246
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
210
247
  }
211
248
  if (occurrences.length) {
212
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
249
+ return {
250
+ ruleId: rule.ruleId,
251
+ outcome: 'fail',
252
+ severity: rule.defaultSeverity || 'serious',
253
+ occurrences
254
+ };
213
255
  }
214
256
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
215
257
  }
216
258
 
217
- module.exports = { id, meta, runInPage };
259
+ module.exports = { id, meta, runInPage };