@binclusive/a11y 0.1.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.
- package/README.md +285 -0
- package/bin/a11y.mjs +36 -0
- package/bin/diff-scope.mjs +29 -0
- package/data/baseline-rules.json +892 -0
- package/package.json +68 -0
- package/src/agent-lane.ts +138 -0
- package/src/agents-block.ts +157 -0
- package/src/baseline/gen-baseline.ts +166 -0
- package/src/cli.ts +1026 -0
- package/src/collect-dom.ts +119 -0
- package/src/collect-liquid.ts +103 -0
- package/src/collect-swift.ts +227 -0
- package/src/collect-unity.ts +99 -0
- package/src/collect.ts +54 -0
- package/src/commands.ts +314 -0
- package/src/config-scan.ts +177 -0
- package/src/contract.ts +355 -0
- package/src/core.ts +546 -0
- package/src/detect-stack.ts +207 -0
- package/src/diff-scope-cli.ts +12 -0
- package/src/diff-scope.ts +150 -0
- package/src/emit-contract.ts +181 -0
- package/src/enforce.ts +1125 -0
- package/src/eslint-plugin-jsx-a11y.d.ts +11 -0
- package/src/evidence.ts +308 -0
- package/src/github-identity.ts +201 -0
- package/src/hook.ts +242 -0
- package/src/impact-gate.ts +107 -0
- package/src/imports-resolve.ts +248 -0
- package/src/index.ts +183 -0
- package/src/liquid-ast.ts +203 -0
- package/src/liquid-rules.ts +691 -0
- package/src/mcp.ts +363 -0
- package/src/module-scope.ts +137 -0
- package/src/phone-home.ts +470 -0
- package/src/pr-comment.ts +250 -0
- package/src/pr-summary-cli.ts +182 -0
- package/src/pr-summary.ts +291 -0
- package/src/registry.ts +605 -0
- package/src/reporter/contract.ts +154 -0
- package/src/reporter/finding.ts +87 -0
- package/src/reporter/github-adapter.ts +183 -0
- package/src/reporter/null-adapter.ts +51 -0
- package/src/reporter/registry.ts +22 -0
- package/src/reporter-cli.ts +48 -0
- package/src/resolve-components.ts +579 -0
- package/src/runner/budget.ts +90 -0
- package/src/runner/codegraph-lookup.test.ts +93 -0
- package/src/runner/codegraph-lookup.ts +197 -0
- package/src/runner/index.ts +86 -0
- package/src/runner/lookup.ts +69 -0
- package/src/runner/provider.ts +72 -0
- package/src/runner/providers/anthropic.ts +168 -0
- package/src/runner/providers/openai.ts +191 -0
- package/src/runner/reasoner.ts +73 -0
- package/src/runner/reasoning/index.ts +53 -0
- package/src/runner/reasoning/prompt.ts +200 -0
- package/src/runner/reasoning/react.ts +149 -0
- package/src/runner/reasoning/shopify.ts +214 -0
- package/src/runner/reasoning/skills-reasoner.ts +117 -0
- package/src/runner/reasoning/types.ts +99 -0
- package/src/runner/runner.ts +203 -0
- package/src/sarif.ts +148 -0
- package/src/source-identity.ts +0 -0
- package/src/source-trace.ts +814 -0
- package/src/suggest.ts +328 -0
- package/src/suppression-ranges.ts +354 -0
- package/src/suppressor-map.ts +189 -0
- package/src/suppressors.ts +284 -0
- package/src/tsconfig-aliases.ts +155 -0
- package/src/unity-ast.ts +331 -0
- package/src/unity-findings.ts +91 -0
- package/src/unity-guid-registry.ts +82 -0
- package/src/unity-label-resolve.ts +249 -0
- package/src/unity-rule-color-only.ts +127 -0
- package/src/unity-rule-missing-label.ts +156 -0
- package/src/unity-rules-baseline.ts +273 -0
- package/src/wcag-map.ts +35 -0
- package/src/wcag-tags.ts +35 -0
- package/src/workspace-resolve.ts +405 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Unity PROJECT-LEVEL structural-absence rule set (#72, child of #66) — the
|
|
3
|
+
* cheapest, highest-frequency negative findings for a Unity game, the analog of the
|
|
4
|
+
* Liquid structural-absence rules (`liquid-rules.ts`, #47/L2) one altitude up.
|
|
5
|
+
*
|
|
6
|
+
* Where the Liquid/uGUI rules are PER-WIDGET (one finding per offending node), these
|
|
7
|
+
* are WHOLE-PROJECT checks: each scans the entire pinned checkout once and emits ONE
|
|
8
|
+
* project-scoped finding, anchored on the project root, not a per-file flood. Two
|
|
9
|
+
* rules ship here:
|
|
10
|
+
*
|
|
11
|
+
* 1. `unity/no-screen-reader-support` — zero references to the hand-authored
|
|
12
|
+
* `UnityEngine.Accessibility` surface (`AccessibilityHierarchy` /
|
|
13
|
+
* `AccessibilityNode` / `AssistiveSupport`) anywhere in the project's `.cs`
|
|
14
|
+
* sources. Unity auto-bridges nothing into an accessibility tree for either uGUI
|
|
15
|
+
* or UI Toolkit, so absence = "the game is unusable with a screen reader." This
|
|
16
|
+
* is grounded: `UnityTechnologies/open-project-1` @ 608eac9 has zero such
|
|
17
|
+
* references across the whole repo (#66 ground truth).
|
|
18
|
+
* 2. `unity/no-input-rebinding` — no `.inputactions` asset anywhere AND no
|
|
19
|
+
* `PerformInteractiveRebinding` call in `.cs`, i.e. no path for a player to
|
|
20
|
+
* remap controls (a WCAG-relevant motor-accessibility gap).
|
|
21
|
+
*
|
|
22
|
+
* Each rule maps to a WCAG SC via the bridge below (the analog of
|
|
23
|
+
* `liquid-rules.ts`'s `RULE_WCAG` and `wcag-tags.ts`), and emits a
|
|
24
|
+
* {@link UnityProjectFinding} that mirrors the canonical `Finding` shape (file, line,
|
|
25
|
+
* ruleId, message, wcag, enforcement, provenance) so the future corpus harness (#69)
|
|
26
|
+
* and report can treat it uniformly. This module is intentionally SELF-CONTAINED and
|
|
27
|
+
* is not yet wired into any shared CLI/dispatch — that wiring is a separate child.
|
|
28
|
+
*
|
|
29
|
+
* Forgiving by construction (mirrors `scanUnity`): a missing project dir is an empty
|
|
30
|
+
* scan (no findings, never a throw), and one unreadable file never aborts the walk.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
34
|
+
import { join, resolve } from "node:path";
|
|
35
|
+
import type { EnforcementLevel } from "./config-scan";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build/library dirs that are not project source — skipped on the walk, mirroring
|
|
39
|
+
* `collect-unity.ts`'s `SKIP_DIRS`. `Library` and `Temp` are Unity's generated
|
|
40
|
+
* caches; an Accessibility reference that lives only in a generated cache is not the
|
|
41
|
+
* project authoring screen-reader support, so excluding them keeps the rule honest.
|
|
42
|
+
*/
|
|
43
|
+
const SKIP_DIRS: ReadonlySet<string> = new Set([
|
|
44
|
+
"node_modules",
|
|
45
|
+
".git",
|
|
46
|
+
"dist",
|
|
47
|
+
"build",
|
|
48
|
+
".cache",
|
|
49
|
+
"Library",
|
|
50
|
+
"Temp",
|
|
51
|
+
"obj",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A project-scoped accessibility finding. Mirrors the canonical `Finding` shape
|
|
56
|
+
* (`core.ts`) field-for-field on the surface the report and enforcement gate read —
|
|
57
|
+
* but is self-contained to this module (no import of `Finding`) so its file surface
|
|
58
|
+
* stays disjoint from the sibling per-widget Unity rules until a later child wires a
|
|
59
|
+
* shared dispatch. `provenance` is the fixed literal `"unity"`, the producer tag the
|
|
60
|
+
* Unity static pass carries; `line` is `0` because a whole-project finding has no
|
|
61
|
+
* single source line (it is anchored on the project root in `file`).
|
|
62
|
+
*/
|
|
63
|
+
export interface UnityProjectFinding {
|
|
64
|
+
/** The project root directory the finding is scoped to (absolute, resolved). */
|
|
65
|
+
readonly file: string;
|
|
66
|
+
/** Always `0` — a project-level finding has no single source line. */
|
|
67
|
+
readonly line: 0;
|
|
68
|
+
/** This rule's stable id (e.g. `unity/no-screen-reader-support`). */
|
|
69
|
+
readonly ruleId: string;
|
|
70
|
+
/** Human-readable description of the absence and its accessibility impact. */
|
|
71
|
+
readonly message: string;
|
|
72
|
+
/** The WCAG success criteria this rule maps to (via {@link wcagForUnityRule}). */
|
|
73
|
+
readonly wcag: readonly string[];
|
|
74
|
+
/** The enforcement level for this finding (defaults to `block`). */
|
|
75
|
+
readonly enforcement: EnforcementLevel;
|
|
76
|
+
/** The producer tag — always `"unity"` for the Unity static pass. */
|
|
77
|
+
readonly provenance: "unity";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The WCAG SC bridge: each Unity project-rule id → its success criteria. The analog
|
|
82
|
+
* of `liquid-rules.ts`'s `RULE_WCAG` (our rule ids are our own, so the mapping is
|
|
83
|
+
* direct). A no-screen-reader-support gap breaks Name/Role/Value (4.1.2) — controls
|
|
84
|
+
* expose no name/role to assistive tech — and Info-and-Relationships (1.3.1); a
|
|
85
|
+
* no-rebinding gap is the motor-accessibility floor, Keyboard (2.1.1) and Pointer
|
|
86
|
+
* Gestures / single-pointer remappability (2.5.1).
|
|
87
|
+
*/
|
|
88
|
+
const RULE_WCAG: Readonly<Record<string, readonly string[]>> = {
|
|
89
|
+
"unity/no-screen-reader-support": ["1.3.1", "4.1.2"],
|
|
90
|
+
"unity/no-input-rebinding": ["2.1.1", "2.5.1"],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** WCAG SCs for a Unity project-rule id (empty if unknown — never throws). */
|
|
94
|
+
export function wcagForUnityRule(ruleId: string): readonly string[] {
|
|
95
|
+
return RULE_WCAG[ruleId] ?? [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Build a project-scoped finding with its rule's WCAG mapping pre-attached. */
|
|
99
|
+
function makeFinding(
|
|
100
|
+
root: string,
|
|
101
|
+
ruleId: string,
|
|
102
|
+
message: string,
|
|
103
|
+
enforcement: EnforcementLevel,
|
|
104
|
+
): UnityProjectFinding {
|
|
105
|
+
return {
|
|
106
|
+
file: root,
|
|
107
|
+
line: 0,
|
|
108
|
+
ruleId,
|
|
109
|
+
message,
|
|
110
|
+
wcag: RULE_WCAG[ruleId] ?? [],
|
|
111
|
+
enforcement,
|
|
112
|
+
provenance: "unity",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** File extensions the project-level scan inspects, by concern. */
|
|
117
|
+
const CS_EXTENSION = ".cs";
|
|
118
|
+
const INPUTACTIONS_EXTENSION = ".inputactions";
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Any reference to the hand-authored `UnityEngine.Accessibility` module surface.
|
|
122
|
+
* Word-boundary anchored so an unrelated identifier that merely contains the substring
|
|
123
|
+
* is not a false match. These three types are the load-bearing surface a project must
|
|
124
|
+
* touch to register a screen-reader accessibility tree.
|
|
125
|
+
*
|
|
126
|
+
* Deliberately conservative: a match anywhere in `.cs` source — including a comment —
|
|
127
|
+
* is treated as a (present) reference, so the absence rule UNDER-reports rather than
|
|
128
|
+
* risk a false positive. A false "no screen-reader support" on a project that does
|
|
129
|
+
* author it is the failure mode that gets the tool uninstalled (the precision
|
|
130
|
+
* invariant the Liquid rules share); under-reporting on a project that only *mentions*
|
|
131
|
+
* the API in a comment is the safe direction.
|
|
132
|
+
*/
|
|
133
|
+
const ACCESSIBILITY_API_RE = /\b(AccessibilityHierarchy|AccessibilityNode|AssistiveSupport)\b/;
|
|
134
|
+
|
|
135
|
+
/** The Input System call that drives interactive control rebinding. */
|
|
136
|
+
const REBINDING_API_RE = /\bPerformInteractiveRebinding\b/;
|
|
137
|
+
|
|
138
|
+
/** What the one-pass project walk gathers — the evidence both rules reason over. */
|
|
139
|
+
interface ProjectEvidence {
|
|
140
|
+
/** True if any non-skipped `.cs` references the `UnityEngine.Accessibility` surface. */
|
|
141
|
+
hasAccessibilityRef: boolean;
|
|
142
|
+
/** True if any `.inputactions` asset exists outside the skipped dirs. */
|
|
143
|
+
hasInputActionsAsset: boolean;
|
|
144
|
+
/** True if any non-skipped `.cs` calls `PerformInteractiveRebinding`. */
|
|
145
|
+
hasRebindingCall: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One forgiving recursive walk gathering the evidence both rules need. Skips the
|
|
150
|
+
* generated-cache dirs (`SKIP_DIRS`), reads each `.cs` once for both string probes,
|
|
151
|
+
* and notes the presence of any `.inputactions` asset. A missing/unreadable directory
|
|
152
|
+
* yields the empty evidence rather than throwing; one unreadable file is skipped, not
|
|
153
|
+
* fatal — the same forgiving contract `scanUnity` gives the CLI.
|
|
154
|
+
*
|
|
155
|
+
* Short-circuits each probe once satisfied: once all three signals are positive the
|
|
156
|
+
* walk can stop reading file contents (it still need not, but avoids needless I/O).
|
|
157
|
+
*/
|
|
158
|
+
async function gatherEvidence(dir: string, evidence: ProjectEvidence): Promise<void> {
|
|
159
|
+
let entries: Awaited<ReturnType<typeof readdir>>;
|
|
160
|
+
try {
|
|
161
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
const full = join(dir, entry.name);
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
169
|
+
await gatherEvidence(full, evidence);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!entry.isFile()) continue;
|
|
173
|
+
|
|
174
|
+
if (entry.name.endsWith(INPUTACTIONS_EXTENSION)) {
|
|
175
|
+
evidence.hasInputActionsAsset = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (entry.name.endsWith(CS_EXTENSION)) {
|
|
179
|
+
// Only read if a probe still needs an answer — once both `.cs` signals are
|
|
180
|
+
// positive, file contents can't change the verdict.
|
|
181
|
+
if (evidence.hasAccessibilityRef && evidence.hasRebindingCall) continue;
|
|
182
|
+
let source: string;
|
|
183
|
+
try {
|
|
184
|
+
source = await readFile(full, "utf8");
|
|
185
|
+
} catch {
|
|
186
|
+
continue; // one unreadable file never aborts the scan
|
|
187
|
+
}
|
|
188
|
+
if (!evidence.hasAccessibilityRef && ACCESSIBILITY_API_RE.test(source)) {
|
|
189
|
+
evidence.hasAccessibilityRef = true;
|
|
190
|
+
}
|
|
191
|
+
if (!evidence.hasRebindingCall && REBINDING_API_RE.test(source)) {
|
|
192
|
+
evidence.hasRebindingCall = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Options for the project-level scan; `enforcement` defaults to `block`. */
|
|
199
|
+
export interface UnityBaselineOptions {
|
|
200
|
+
readonly enforcement?: EnforcementLevel;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Run the Unity project-level structural-absence rules over a whole project directory,
|
|
205
|
+
* returning the project-scoped findings (zero, one, or two). The single entry point
|
|
206
|
+
* the future corpus harness (#69) calls per project.
|
|
207
|
+
*
|
|
208
|
+
* - `unity/no-screen-reader-support` fires iff NO `.cs` references the
|
|
209
|
+
* `UnityEngine.Accessibility` surface anywhere in the project.
|
|
210
|
+
* - `unity/no-input-rebinding` fires iff the project has NEITHER a `.inputactions`
|
|
211
|
+
* asset NOR a `PerformInteractiveRebinding` call (either present → silent).
|
|
212
|
+
*
|
|
213
|
+
* A missing directory yields `[]` (an empty scan), never a throw.
|
|
214
|
+
*/
|
|
215
|
+
export async function runUnityBaselineRules(
|
|
216
|
+
dir: string,
|
|
217
|
+
options: UnityBaselineOptions = {},
|
|
218
|
+
): Promise<UnityProjectFinding[]> {
|
|
219
|
+
const root = resolve(dir);
|
|
220
|
+
const enforcement: EnforcementLevel = options.enforcement ?? "block";
|
|
221
|
+
|
|
222
|
+
// A non-existent project root is "no project to evaluate", not "a project with
|
|
223
|
+
// everything absent" — return an empty scan rather than firing every absence rule
|
|
224
|
+
// on nothing. (An EXISTING but empty/Accessibility-free project still fires, which
|
|
225
|
+
// is the #66 ground-truth case.)
|
|
226
|
+
try {
|
|
227
|
+
const info = await stat(root);
|
|
228
|
+
if (!info.isDirectory()) return [];
|
|
229
|
+
} catch {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const evidence: ProjectEvidence = {
|
|
234
|
+
hasAccessibilityRef: false,
|
|
235
|
+
hasInputActionsAsset: false,
|
|
236
|
+
hasRebindingCall: false,
|
|
237
|
+
};
|
|
238
|
+
await gatherEvidence(root, evidence);
|
|
239
|
+
|
|
240
|
+
const findings: UnityProjectFinding[] = [];
|
|
241
|
+
|
|
242
|
+
if (!evidence.hasAccessibilityRef) {
|
|
243
|
+
findings.push(
|
|
244
|
+
makeFinding(
|
|
245
|
+
root,
|
|
246
|
+
"unity/no-screen-reader-support",
|
|
247
|
+
"No screen-reader support present — the project references none of " +
|
|
248
|
+
"`AccessibilityHierarchy`, `AccessibilityNode`, or `AssistiveSupport` " +
|
|
249
|
+
"(the `UnityEngine.Accessibility` API). Unity bridges nothing into an " +
|
|
250
|
+
"accessibility tree automatically, so the game is unusable with a screen " +
|
|
251
|
+
"reader. Build an `AccessibilityHierarchy` of `AccessibilityNode`s and " +
|
|
252
|
+
"register it via `AssistiveSupport`.",
|
|
253
|
+
enforcement,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!evidence.hasInputActionsAsset && !evidence.hasRebindingCall) {
|
|
259
|
+
findings.push(
|
|
260
|
+
makeFinding(
|
|
261
|
+
root,
|
|
262
|
+
"unity/no-input-rebinding",
|
|
263
|
+
"No input remapping present — the project ships no `.inputactions` asset and " +
|
|
264
|
+
"makes no `PerformInteractiveRebinding` call, so a player cannot remap " +
|
|
265
|
+
"controls. Fixed controls are a motor-accessibility barrier; expose a " +
|
|
266
|
+
"rebinding path via the Input System.",
|
|
267
|
+
enforcement,
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return findings;
|
|
273
|
+
}
|
package/src/wcag-map.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jsx-a11y rule id -> WCAG Success Criteria.
|
|
3
|
+
*
|
|
4
|
+
* The map is the bridge between the lint engine's vocabulary (rule ids) and
|
|
5
|
+
* Binclusive's audit vocabulary (WCAG SC numbers). The corpus cross-ref
|
|
6
|
+
* (see `corpus.ts`) keys off the SC, so this map is what lets a source-level
|
|
7
|
+
* lint hit inherit real-world frequency from the dynamic-audit corpus.
|
|
8
|
+
*
|
|
9
|
+
* Rule ids are unprefixed here; the engine emits them prefixed as
|
|
10
|
+
* `jsx-a11y/<id>`. `wcagForRuleId` strips the prefix before lookup.
|
|
11
|
+
*/
|
|
12
|
+
export const RULE_ID_TO_WCAG: Readonly<Record<string, readonly string[]>> = {
|
|
13
|
+
"label-has-associated-control": ["1.3.1", "4.1.2"],
|
|
14
|
+
"alt-text": ["1.1.1"],
|
|
15
|
+
"anchor-has-content": ["2.4.4"],
|
|
16
|
+
"anchor-is-valid": ["2.4.4"],
|
|
17
|
+
"aria-props": ["4.1.2"],
|
|
18
|
+
"role-has-required-aria-props": ["4.1.2"],
|
|
19
|
+
"role-supports-aria-props": ["4.1.2"],
|
|
20
|
+
"interactive-supports-focus": ["2.1.1"],
|
|
21
|
+
"click-events-have-key-events": ["2.1.1"],
|
|
22
|
+
"no-static-element-interactions": ["2.1.1"],
|
|
23
|
+
"heading-has-content": ["1.3.1"],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Look up the WCAG SC for a (possibly `jsx-a11y/`-prefixed) rule id.
|
|
28
|
+
* Returns `[]` for rules we have not mapped — the finding still reports,
|
|
29
|
+
* it just carries no SC and therefore no corpus enrichment.
|
|
30
|
+
*/
|
|
31
|
+
export function wcagForRuleId(ruleId: string | null): readonly string[] {
|
|
32
|
+
if (ruleId === null) return [];
|
|
33
|
+
const bare = ruleId.startsWith("jsx-a11y/") ? ruleId.slice("jsx-a11y/".length) : ruleId;
|
|
34
|
+
return RULE_ID_TO_WCAG[bare] ?? [];
|
|
35
|
+
}
|
package/src/wcag-tags.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one bridge between axe-core's tag vocabulary and the corpus' WCAG SC keys.
|
|
3
|
+
*
|
|
4
|
+
* Pure — no browser, no playwright, no axe runtime. Lives apart from
|
|
5
|
+
* `collect-dom.ts` so BOTH the live-DOM collector and the offline
|
|
6
|
+
* baseline-catalog generator (`src/baseline/gen-baseline.ts`) read SCs off axe
|
|
7
|
+
* tags through the EXACT same function. `collect-dom.ts` re-exports `scFromTags`
|
|
8
|
+
* for back-compat, so existing imports keep working.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read WCAG success criteria off axe-core's `tags`. axe tags an SC as
|
|
13
|
+
* `wcag<principle><guideline><criterion>` with the dots removed — `wcag111` is
|
|
14
|
+
* 1.1.1, `wcag244` is 2.4.4, `wcag1411` is 1.4.11. The principle and guideline
|
|
15
|
+
* are always one digit; the criterion is the remainder (so 2-or-more-digit
|
|
16
|
+
* criteria like `.11` round-trip). Conformance-level tags (`wcag2a`, `wcag21aa`)
|
|
17
|
+
* and non-WCAG tags (`best-practice`, `cat.color`, `ACT`) carry letters and are
|
|
18
|
+
* skipped. Deduped, original order preserved.
|
|
19
|
+
*
|
|
20
|
+
* This is the whole bridge between axe's vocabulary and the corpus: every
|
|
21
|
+
* `enrichAll` lookup — and the baseline catalog — keys off these SC strings.
|
|
22
|
+
*/
|
|
23
|
+
export function scFromTags(tags: readonly string[]): string[] {
|
|
24
|
+
const out: string[] = [];
|
|
25
|
+
const seen = new Set<string>();
|
|
26
|
+
for (const tag of tags) {
|
|
27
|
+
const m = /^wcag(\d)(\d)(\d+)$/.exec(tag);
|
|
28
|
+
if (m === null) continue;
|
|
29
|
+
const sc = `${m[1]}.${m[2]}.${m[3]}`;
|
|
30
|
+
if (seen.has(sc)) continue;
|
|
31
|
+
seen.add(sc);
|
|
32
|
+
out.push(sc);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|