@surea11y/core 1.1.2 → 1.3.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 (160) hide show
  1. package/CHANGELOG.md +48 -4
  2. package/LICENSE +373 -21
  3. package/README.md +70 -1
  4. package/bin/core.js +240 -11
  5. package/docs/API_STABILITY.md +61 -0
  6. package/docs/BASELINE.md +66 -0
  7. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  8. package/docs/CI_INTEGRATIONS.md +103 -0
  9. package/docs/CLI.md +80 -1
  10. package/docs/ENGINE_OPTIONS.md +4 -0
  11. package/docs/INTEGRATION.md +19 -1
  12. package/docs/OUTPUT_SCHEMA.md +2 -2
  13. package/docs/REPORT.md +33 -0
  14. package/docs/RULE_AUTHORING.md +31 -0
  15. package/docs/RULE_CATALOG.md +1 -1
  16. package/docs/SARIF.md +59 -0
  17. package/package.json +15 -4
  18. package/src/baseline.js +0 -0
  19. package/src/catalogs/composites.wcag.js +414 -450
  20. package/src/checks/automatic/area-alt-present.js +59 -25
  21. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  22. package/src/checks/automatic/aria-allowed-role.js +21 -7
  23. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  24. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  25. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  26. package/src/checks/automatic/aria-hidden-body.js +50 -19
  27. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  28. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  29. package/src/checks/automatic/aria-prohibited-children.js +125 -29
  30. package/src/checks/automatic/aria-required-attr.js +23 -8
  31. package/src/checks/automatic/aria-required-children.js +37 -14
  32. package/src/checks/automatic/aria-required-parent.js +48 -14
  33. package/src/checks/automatic/aria-role-name-present.js +47 -21
  34. package/src/checks/automatic/aria-roles-valid.js +22 -12
  35. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  36. package/src/checks/automatic/aria-valid-attr.js +17 -5
  37. package/src/checks/automatic/autocomplete-valid.js +74 -16
  38. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  39. package/src/checks/automatic/binary-control-name-present.js +60 -50
  40. package/src/checks/automatic/button-name-present.js +48 -18
  41. package/src/checks/automatic/bypass-blocks-present.js +50 -25
  42. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  43. package/src/checks/automatic/combobox-name-present.js +38 -45
  44. package/src/checks/automatic/contrast-computable.js +361 -341
  45. package/src/checks/automatic/contrast-enhanced.js +487 -466
  46. package/src/checks/automatic/contrast-minimum.js +486 -465
  47. package/src/checks/automatic/css-orientation-lock.js +39 -9
  48. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  49. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  50. package/src/checks/automatic/dialog-name-present.js +37 -75
  51. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  52. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  53. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  54. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  55. package/src/checks/automatic/form-control-single-label.js +47 -10
  56. package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
  57. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  58. package/src/checks/automatic/iframe-name-present.js +31 -9
  59. package/src/checks/automatic/iframe-title-unique.js +29 -8
  60. package/src/checks/automatic/img-alt-present.js +47 -43
  61. package/src/checks/automatic/input-image-alt-present.js +143 -112
  62. package/src/checks/automatic/label-in-name.js +50 -22
  63. package/src/checks/automatic/language-page-present.js +117 -109
  64. package/src/checks/automatic/link-in-text-block.js +59 -19
  65. package/src/checks/automatic/link-name-present.js +45 -14
  66. package/src/checks/automatic/list-children-valid.js +26 -9
  67. package/src/checks/automatic/listbox-name-present.js +39 -19
  68. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  69. package/src/checks/automatic/menuitem-name-present.js +39 -61
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
  72. package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
  73. package/src/checks/automatic/meter-name-present.js +36 -33
  74. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  75. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  76. package/src/checks/automatic/option-name-present.js +38 -21
  77. package/src/checks/automatic/page-title-present.js +26 -7
  78. package/src/checks/automatic/progressbar-name-present.js +41 -34
  79. package/src/checks/automatic/role-img-alt-present.js +209 -157
  80. package/src/checks/automatic/searchbox-name-present.js +39 -19
  81. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  82. package/src/checks/automatic/slider-name-present.js +40 -47
  83. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  84. package/src/checks/automatic/summary-name-present.js +37 -17
  85. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  86. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  87. package/src/checks/automatic/tab-name-present.js +37 -60
  88. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  89. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  90. package/src/checks/automatic/target-size-minimum.js +118 -48
  91. package/src/checks/automatic/td-has-header.js +29 -11
  92. package/src/checks/automatic/textbox-name-present.js +39 -19
  93. package/src/checks/automatic/tooltip-name-present.js +37 -18
  94. package/src/checks/automatic/treeitem-name-present.js +38 -21
  95. package/src/checks/automatic/valid-lang.js +20 -6
  96. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  97. package/src/checks/manual/accesskeys-manual.js +14 -5
  98. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  99. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  100. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  101. package/src/checks/manual/aria-text-manual.js +14 -6
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  103. package/src/checks/manual/css-hidden-focus.js +196 -165
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  105. package/src/checks/manual/empty-heading-manual.js +24 -12
  106. package/src/checks/manual/empty-table-header-manual.js +21 -14
  107. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  108. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  109. package/src/checks/manual/heading-order-manual.js +22 -12
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  111. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  112. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  113. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  115. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  116. package/src/checks/manual/label-title-only-manual.js +14 -5
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  123. package/src/checks/manual/landmark-one-main-manual.js +35 -21
  124. package/src/checks/manual/landmark-unique-manual.js +73 -45
  125. package/src/checks/manual/link-name-quality-manual.js +43 -12
  126. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  127. package/src/checks/manual/meta-viewport-large-manual.js +25 -6
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  129. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  131. package/src/checks/manual/p-as-heading-manual.js +22 -7
  132. package/src/checks/manual/page-has-heading-one-manual.js +40 -23
  133. package/src/checks/manual/page-title-patterns-manual.js +87 -51
  134. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  135. package/src/checks/manual/region-manual.js +275 -62
  136. package/src/checks/manual/scope-attr-valid-manual.js +11 -9
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  138. package/src/checks/manual/skip-link-manual.js +35 -12
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  140. package/src/checks/manual/tabindex-manual.js +11 -9
  141. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  142. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  143. package/src/checks/manual/video-caption-manual.js +15 -4
  144. package/src/checks/manual-review.js +56 -12
  145. package/src/core/aria-helpers.js +1128 -823
  146. package/src/core/contrast-helpers.js +1217 -1062
  147. package/src/core/dom-helpers.js +4176 -3886
  148. package/src/core/dom-runner.js +720 -596
  149. package/src/core/frame-messaging.js +189 -138
  150. package/src/core/frame-scan.js +94 -82
  151. package/src/core/rollup-composites.js +94 -102
  152. package/src/core/rule-meta.js +71 -35
  153. package/src/core.js +37939 -29121
  154. package/src/i18n/en.js +1194 -887
  155. package/src/i18n/fr.js +1136 -793
  156. package/src/policy/contracts.js +13 -13
  157. package/src/policy/resolvePolicy.js +48 -44
  158. package/src/report.js +502 -0
  159. package/src/sarif.js +175 -0
  160. package/surea11y.browser.js +36042 -0
package/src/sarif.js ADDED
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Renders one scan result as a SARIF 2.1.0 log (docs/SARIF.md) for GitHub
5
+ * Code Scanning / other SARIF-consuming dashboards -- a different output
6
+ * shape from --json's raw result, purpose-built for that ecosystem (see
7
+ * docs/CLI.md's --sarif flag).
8
+ *
9
+ * Only fail/cantTell occurrences become SARIF results (a pass/notApplicable
10
+ * result has no occurrences at all -- see docs/OUTPUT_SCHEMA.md), the same
11
+ * "violations only" framing docs/REPORT.md's HTML report already uses.
12
+ * fail -> SARIF level "error" (the CI-gating case); cantTell -> "warning"
13
+ * (surfaced, non-blocking -- this engine's own cantTell/manual-review mental
14
+ * model, docs/TROUBLESHOOTING.md).
15
+ *
16
+ * If `baselineEntries` is supplied (mirrors --baseline, docs/BASELINE.md),
17
+ * fail occurrences already recorded there are omitted entirely rather than
18
+ * downgraded -- a generic SARIF consumer has no "known, don't re-gate"
19
+ * concept of its own, so the only faithful way to honor a baseline here is
20
+ * to not report the occurrence at all. cantTell occurrences are never
21
+ * baseline-filtered (the baseline mechanism only ever tracks fail
22
+ * occurrences, matching --write-baseline).
23
+ */
24
+
25
+ const path = require('path');
26
+ const { computeBaselineKey, getReasonCode } = require('./baseline.js');
27
+
28
+ const SARIF_SCHEMA_URI =
29
+ 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/Schemata/sarif-schema-2.1.0.json';
30
+ const SARIF_VERSION = '2.1.0';
31
+
32
+ function artifactUriFromResult(result) {
33
+ const url = result && result.url;
34
+ if (!url) return 'about:blank';
35
+ if (url.startsWith('file://')) {
36
+ const filePath = url.slice('file://'.length);
37
+ const rel = path.relative(process.cwd(), filePath).split(path.sep).join('/');
38
+ // A file outside the cwd (e.g. an absolute path elsewhere on disk)
39
+ // produces a "../"-leading relative path -- still valid as a URI, just
40
+ // not resolvable as a repo-relative one by a SARIF consumer like GitHub
41
+ // Code Scanning (see docs/SARIF.md's known limitations).
42
+ return rel || filePath;
43
+ }
44
+ return url;
45
+ }
46
+
47
+ function buildRemainingBaselineMap(baselineEntries) {
48
+ const remaining = new Map();
49
+ for (const entry of Array.isArray(baselineEntries) ? baselineEntries : []) {
50
+ if (!entry) continue;
51
+ const key = computeBaselineKey(
52
+ entry.ruleId,
53
+ entry.reasonCode || 'DEFAULT',
54
+ typeof entry.html === 'string' ? entry.html : ''
55
+ );
56
+ remaining.set(key, (remaining.get(key) || 0) + 1);
57
+ }
58
+ return remaining;
59
+ }
60
+
61
+ function wcagTags(check) {
62
+ const mappings = (check.meta && check.meta.normativeMappings) || [];
63
+ const tags = new Set(['accessibility', check.type === 'automatic' ? 'automatic' : 'manual']);
64
+ for (const m of mappings) {
65
+ if (m && m.requirement) tags.add(`wcag-${m.requirement}`);
66
+ }
67
+ return Array.from(tags);
68
+ }
69
+
70
+ function buildRule(check) {
71
+ return {
72
+ id: check.ruleId,
73
+ name: check.ruleId,
74
+ shortDescription: { text: check.title || check.ruleId },
75
+ fullDescription: { text: check.description || check.title || check.ruleId },
76
+ // type: "manual" rules are capped at cantTell (never fail), so their
77
+ // worst-case, rule-level default is "warning"; automatic rules can
78
+ // reach "error" -- see docs/OUTPUT_SCHEMA.md's outcome/type table.
79
+ defaultConfiguration: { level: check.type === 'automatic' ? 'error' : 'warning' },
80
+ properties: { tags: wcagTags(check) }
81
+ };
82
+ }
83
+
84
+ function buildResult(check, occurrence, level, artifactUri) {
85
+ const reasonCode = getReasonCode(occurrence);
86
+ const html = typeof occurrence.html === 'string' ? occurrence.html : '';
87
+ const message = occurrence.hint ? `${occurrence.summary} ${occurrence.hint}` : occurrence.summary;
88
+
89
+ return {
90
+ ruleId: check.ruleId,
91
+ level,
92
+ message: { text: message },
93
+ locations: [
94
+ {
95
+ physicalLocation: { artifactLocation: { uri: artifactUri } },
96
+ ...(occurrence.selector
97
+ ? { logicalLocations: [{ fullyQualifiedName: occurrence.selector, kind: 'element' }] }
98
+ : {})
99
+ }
100
+ ],
101
+ partialFingerprints: {
102
+ 'surea11y/violation/v1': computeBaselineKey(check.ruleId, reasonCode, html)
103
+ },
104
+ properties: {
105
+ severity: check.severity,
106
+ confidence: check.confidence,
107
+ reasonCode,
108
+ html
109
+ }
110
+ };
111
+ }
112
+
113
+ function renderSarifReport(result, options = {}) {
114
+ const { toolVersion, informationUri, baselineEntries } = options;
115
+ const artifactUri = artifactUriFromResult(result);
116
+ const remaining = buildRemainingBaselineMap(baselineEntries);
117
+
118
+ const rules = [];
119
+ const seenRuleIds = new Set();
120
+ const failResults = [];
121
+ const cantTellResults = [];
122
+
123
+ for (const check of (result && result.checksResults) || []) {
124
+ if (!check || !Array.isArray(check.occurrences)) continue;
125
+
126
+ if (!seenRuleIds.has(check.ruleId)) {
127
+ seenRuleIds.add(check.ruleId);
128
+ rules.push(buildRule(check));
129
+ }
130
+
131
+ if (check.outcome !== 'fail' && check.outcome !== 'cantTell') continue;
132
+
133
+ for (const occurrence of check.occurrences) {
134
+ if (!occurrence) continue;
135
+
136
+ if (check.outcome === 'fail') {
137
+ const reasonCode = getReasonCode(occurrence);
138
+ const html = typeof occurrence.html === 'string' ? occurrence.html : '';
139
+ const key = computeBaselineKey(check.ruleId, reasonCode, html);
140
+ const left = remaining.get(key) || 0;
141
+ if (left > 0) {
142
+ remaining.set(key, left - 1);
143
+ continue; // already known via the baseline -- omit, don't re-gate
144
+ }
145
+ failResults.push(buildResult(check, occurrence, 'error', artifactUri));
146
+ } else {
147
+ cantTellResults.push(buildResult(check, occurrence, 'warning', artifactUri));
148
+ }
149
+ }
150
+ }
151
+
152
+ const sarifLog = {
153
+ $schema: SARIF_SCHEMA_URI,
154
+ version: SARIF_VERSION,
155
+ runs: [
156
+ {
157
+ tool: {
158
+ driver: {
159
+ name: 'surea11y',
160
+ informationUri: informationUri || 'https://github.com/rumoroso/surea11y-core',
161
+ version: toolVersion || '0.0.0',
162
+ rules
163
+ }
164
+ },
165
+ // fail first: matches docs/REPORT.md's own "violations before advisory
166
+ // findings" ordering.
167
+ results: [...failResults, ...cantTellResults]
168
+ }
169
+ ]
170
+ };
171
+
172
+ return JSON.stringify(sarifLog, null, 2) + '\n';
173
+ }
174
+
175
+ module.exports = { renderSarifReport };