eaa-kit 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.
@@ -0,0 +1,169 @@
1
+ import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
2
+ import { z } from "zod";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ /** Default filename, used by the CLI when no path is given. */
6
+ const DEFAULT_BASELINE_FILE = "eaa-baseline.json";
7
+ const entrySchema = z.object({
8
+ /** Page path relative to the audited directory, POSIX separators. */
9
+ page: z.string(),
10
+ ruleId: z.string(),
11
+ /** Identity of the element, from elementFingerprint. */
12
+ fingerprint: z.string(),
13
+ /** Carried for readability only; matching never looks at these. */
14
+ selector: z.string().default(""),
15
+ help: z.string().default(""),
16
+ impact: z.string().nullable().default(null),
17
+ /** ISO date the entry was written. */
18
+ acceptedOn: z.string().default(""),
19
+ /** ISO date after which this entry stops suppressing anything. */
20
+ expiresOn: z.iso.date().optional(),
21
+ /** Why this is being lived with. Free text, for whoever reads the file. */
22
+ note: z.string().optional()
23
+ });
24
+ const baselineSchema = z.object({
25
+ schemaVersion: z.number(),
26
+ createdOn: z.string().default(""),
27
+ entries: z.array(entrySchema).default([])
28
+ });
29
+ var BaselineError = class extends Error {
30
+ name = "BaselineError";
31
+ };
32
+ /**
33
+ * Move the violations a baseline accounts for out of the failing set.
34
+ *
35
+ * Returns new audit objects; the ones passed in are not touched, so a caller
36
+ * can still report on what the run actually found.
37
+ */
38
+ function applyBaseline(audits, baseline, options = {}) {
39
+ const today = isoDate(options.today ?? /* @__PURE__ */ new Date());
40
+ const expired = baseline.entries.filter((entry) => entry.expiresOn !== void 0 && entry.expiresOn < today);
41
+ const live = /* @__PURE__ */ new Map();
42
+ for (const entry of baseline.entries) {
43
+ if (entry.expiresOn !== void 0 && entry.expiresOn < today) continue;
44
+ live.set(key(entry.page, entry.ruleId, entry.fingerprint), entry);
45
+ }
46
+ const matched = /* @__PURE__ */ new Set();
47
+ let accepted = 0;
48
+ const next = audits.map((audit) => {
49
+ const violations = [];
50
+ const acceptedFindings = [];
51
+ for (const finding of audit.violations) {
52
+ const kept = [];
53
+ const waived = [];
54
+ for (const node of finding.nodes) {
55
+ const id = key(audit.relativePath, finding.ruleId, elementFingerprint(finding.ruleId, node.target.join(" "), node.html));
56
+ if (live.has(id)) {
57
+ matched.add(id);
58
+ waived.push(node);
59
+ } else kept.push(node);
60
+ }
61
+ if (finding.nodes.length === 0) {
62
+ const id = key(audit.relativePath, finding.ruleId, elementFingerprint(finding.ruleId, "", ""));
63
+ if (live.has(id)) {
64
+ matched.add(id);
65
+ acceptedFindings.push(finding);
66
+ continue;
67
+ }
68
+ violations.push(finding);
69
+ continue;
70
+ }
71
+ if (waived.length > 0) {
72
+ accepted += waived.length;
73
+ acceptedFindings.push({
74
+ ...finding,
75
+ nodes: waived
76
+ });
77
+ }
78
+ if (kept.length > 0) violations.push({
79
+ ...finding,
80
+ nodes: kept
81
+ });
82
+ }
83
+ return {
84
+ ...audit,
85
+ violations,
86
+ ...acceptedFindings.length > 0 ? { accepted: acceptedFindings } : {}
87
+ };
88
+ });
89
+ const audited = new Set(audits.map((audit) => audit.relativePath));
90
+ return {
91
+ audits: next,
92
+ stale: [...live.entries()].filter(([id, entry]) => !matched.has(id) && audited.has(entry.page)).map(([, entry]) => entry).sort(byEntry),
93
+ expired: [...expired].sort(byEntry),
94
+ accepted
95
+ };
96
+ }
97
+ /** Record every violation a run found, so a later run can fail only on new ones. */
98
+ function buildBaseline(audits, options = {}) {
99
+ const acceptedOn = isoDate(options.today ?? /* @__PURE__ */ new Date());
100
+ const entries = [];
101
+ for (const audit of audits) for (const finding of audit.violations) {
102
+ const nodes = finding.nodes.length > 0 ? finding.nodes.map((node) => ({
103
+ selector: node.target.join(" "),
104
+ html: node.html
105
+ })) : [{
106
+ selector: "",
107
+ html: ""
108
+ }];
109
+ for (const node of nodes) entries.push({
110
+ page: audit.relativePath,
111
+ ruleId: finding.ruleId,
112
+ fingerprint: elementFingerprint(finding.ruleId, node.selector, node.html),
113
+ selector: node.selector,
114
+ help: finding.help,
115
+ impact: finding.impact ?? null,
116
+ acceptedOn,
117
+ ...options.expiresOn ? { expiresOn: options.expiresOn } : {},
118
+ ...options.note ? { note: options.note } : {}
119
+ });
120
+ }
121
+ return {
122
+ schemaVersion: 1,
123
+ createdOn: acceptedOn,
124
+ entries: entries.sort(byEntry)
125
+ };
126
+ }
127
+ /** Serialised form, with a trailing newline, sorted so it diffs cleanly. */
128
+ function serialiseBaseline(baseline) {
129
+ return `${JSON.stringify(baseline, null, 2)}\n`;
130
+ }
131
+ async function readBaseline(file, cwd = process.cwd()) {
132
+ const target = path.resolve(cwd, file);
133
+ let raw;
134
+ try {
135
+ raw = await readFile(target, "utf8");
136
+ } catch {
137
+ throw new BaselineError(`Could not read the baseline at ${file}. Create one with: eaa-kit baseline`);
138
+ }
139
+ let value;
140
+ try {
141
+ value = JSON.parse(raw);
142
+ } catch (cause) {
143
+ throw new BaselineError(`${path.basename(target)} is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}`);
144
+ }
145
+ const result = baselineSchema.safeParse(value);
146
+ if (!result.success) {
147
+ const issues = result.error.issues.map((issue) => `${issue.path.join(".") || "document"}: ${issue.message}`).slice(0, 5);
148
+ throw new BaselineError(`${path.basename(target)} is not an eaa-kit baseline (${issues.join("; ")})`);
149
+ }
150
+ if (result.data.schemaVersion !== 1) throw new BaselineError(`${path.basename(target)} has schemaVersion ${result.data.schemaVersion}; this version of eaa-kit reads 1`);
151
+ return result.data;
152
+ }
153
+ async function writeBaseline(file, baseline, cwd = process.cwd()) {
154
+ const target = path.resolve(cwd, file);
155
+ await mkdir(path.dirname(target), { recursive: true });
156
+ await writeFile(target, serialiseBaseline(baseline), "utf8");
157
+ return target;
158
+ }
159
+ function key(page, ruleId, fingerprint) {
160
+ return `${page}\u0000${ruleId}\u0000${fingerprint}`;
161
+ }
162
+ function byEntry(a, b) {
163
+ return a.page.localeCompare(b.page) || a.ruleId.localeCompare(b.ruleId) || a.fingerprint.localeCompare(b.fingerprint);
164
+ }
165
+ function isoDate(date) {
166
+ return date.toISOString().slice(0, 10);
167
+ }
168
+ //#endregion
169
+ export { readBaseline as a, buildBaseline as i, DEFAULT_BASELINE_FILE as n, serialiseBaseline as o, applyBaseline as r, writeBaseline as s, BaselineError as t };
@@ -0,0 +1,2 @@
1
+ import { a as readBaseline, r as applyBaseline, t as BaselineError } from "./baseline-DQTnNlc4.js";
2
+ export { BaselineError, applyBaseline, readBaseline };
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ import { f as COUNTRIES, h as STATEMENT_LOCALES, i as readAuditReport, l as loadConfig, o as StatementError, p as ConfigError, t as renderStatement } from "../render-K9KxDDSA.js";
3
+ import { i as isImpactLevel, n as IMPACT_LEVELS, t as DEFAULT_FAIL_ON } from "../impact-DvgBjupx.js";
4
+ import { t as TOOL_VERSION } from "../version-B3v4rNoG.js";
5
+ import { i as buildBaseline, n as DEFAULT_BASELINE_FILE, s as writeBaseline, t as BaselineError } from "../baseline-DQTnNlc4.js";
6
+ import { a as collectPages, i as BuildDirectoryError, n as isOutputFormat, r as runAuditCommand, t as OUTPUT_FORMATS } from "../audit-6gbV0Zjd.js";
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { Command, InvalidArgumentError } from "commander";
10
+ import pc from "picocolors";
11
+ //#region src/cli/baseline.ts
12
+ /**
13
+ * `eaa-kit baseline [dir]`.
14
+ *
15
+ * Runs the same audit the audit command runs and writes down every violation it
16
+ * found, so that a later run can fail on what is new instead of on everything.
17
+ *
18
+ * Deliberately a subcommand rather than a flag on `audit`. Accepting a set of
19
+ * violations is a decision somebody makes once and commits to a file others
20
+ * will read; folding it into the command that checks them would make it far too
21
+ * easy to type by reflex when a build goes red, which is precisely the moment
22
+ * it should take a deliberate act.
23
+ */
24
+ async function runBaselineCommand(dir, options = {}) {
25
+ const cwd = options.cwd ?? process.cwd();
26
+ let pages;
27
+ try {
28
+ pages = await collectPages(path.resolve(cwd, dir), {
29
+ ...options.include ? { include: options.include } : {},
30
+ ...options.exclude ? { exclude: options.exclude } : {}
31
+ });
32
+ } catch (cause) {
33
+ if (cause instanceof BuildDirectoryError) {
34
+ process.stderr.write(`${pc.red("error")} ${cause.message}\n`);
35
+ return {
36
+ entries: 0,
37
+ exitCode: 2
38
+ };
39
+ }
40
+ throw cause;
41
+ }
42
+ if (pages.length === 0) {
43
+ process.stderr.write(`${pc.yellow("warning")} No HTML files found in ${dir}\n`);
44
+ return {
45
+ entries: 0,
46
+ exitCode: 2
47
+ };
48
+ }
49
+ process.stderr.write(pc.dim(`Auditing ${pages.length} ${pages.length === 1 ? "page" : "pages"} in ${dir}…\n`));
50
+ const runnerOptions = {
51
+ ...options.baseUrl ? { baseUrl: options.baseUrl } : {},
52
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
53
+ };
54
+ let audits;
55
+ if (options.browser) {
56
+ const { BrowserUnavailableError, runBrowserAudit } = await import("../playwright-BfWuTG_u.js");
57
+ try {
58
+ audits = await runBrowserAudit(path.resolve(cwd, dir), pages, runnerOptions);
59
+ } catch (cause) {
60
+ if (cause instanceof BrowserUnavailableError) {
61
+ process.stderr.write(`${pc.red("error")} ${cause.message}\n`);
62
+ return {
63
+ entries: 0,
64
+ exitCode: 2
65
+ };
66
+ }
67
+ throw cause;
68
+ }
69
+ } else {
70
+ const { runPooledAudit } = await import("../pool-DixLeu8L.js");
71
+ audits = await runPooledAudit(pages, {
72
+ ...runnerOptions,
73
+ ...options.concurrency === void 0 ? {} : { concurrency: options.concurrency }
74
+ });
75
+ }
76
+ const unaudited = audits.filter((audit) => audit.error);
77
+ if (unaudited.length > 0) {
78
+ process.stderr.write(`${pc.red("error")} ${unaudited.length} of ${audits.length} pages could not be audited, so no baseline was written\n`);
79
+ return {
80
+ entries: 0,
81
+ exitCode: 2
82
+ };
83
+ }
84
+ const baseline = buildBaseline(audits, {
85
+ ...options.note ? { note: options.note } : {},
86
+ ...options.expiresOn ? { expiresOn: options.expiresOn } : {}
87
+ });
88
+ const target = options.output ?? "eaa-baseline.json";
89
+ try {
90
+ await writeBaseline(target, baseline, cwd);
91
+ } catch (cause) {
92
+ if (cause instanceof BaselineError) {
93
+ process.stderr.write(`${pc.red("error")} ${cause.message}\n`);
94
+ return {
95
+ entries: 0,
96
+ exitCode: 2
97
+ };
98
+ }
99
+ throw cause;
100
+ }
101
+ const count = baseline.entries.length;
102
+ process.stderr.write(pc.dim(`Wrote ${count} ${count === 1 ? "entry" : "entries"} to ${target}\n`));
103
+ if (count > 0) process.stderr.write(pc.yellow("These are barriers, not exceptions. Commit the file, then work the list down.\n"));
104
+ return {
105
+ entries: count,
106
+ exitCode: 0
107
+ };
108
+ }
109
+ //#endregion
110
+ //#region src/cli/statement.ts
111
+ /** Markdown for a content directory, HTML for dropping straight onto a site. */
112
+ const STATEMENT_FORMATS = ["markdown", "html"];
113
+ function isStatementFormat(value) {
114
+ return STATEMENT_FORMATS.includes(value);
115
+ }
116
+ /**
117
+ * `eaa-kit statement`.
118
+ *
119
+ * The document goes to stdout and everything else to stderr, so it can be piped
120
+ * straight into a file or a static site's content directory.
121
+ */
122
+ async function runStatementCommand(options = {}) {
123
+ const format = options.format ?? formatFor(options.output);
124
+ try {
125
+ const { config, path: configPath } = await loadConfig({
126
+ ...options.cwd ? { cwd: options.cwd } : {},
127
+ ...options.config ? { path: options.config } : {}
128
+ });
129
+ let audit;
130
+ if (options.audit) audit = await readAuditReport(options.audit, options.cwd ?? process.cwd());
131
+ const statement = await renderStatement(config, {
132
+ ...options.locale ? { locale: options.locale } : {},
133
+ ...options.country ? { country: options.country } : {},
134
+ ...audit ? { audit } : {}
135
+ });
136
+ process.stderr.write(pc.dim(`Statement for ${config.site.url} from ${path.basename(configPath)} (${statement.template}, ${format})\n`));
137
+ if (audit) {
138
+ process.stderr.write(pc.dim(`${audit.findings.length} ${audit.findings.length === 1 ? "barrier" : "barriers"} taken from ${path.basename(options.audit ?? "")}\n`));
139
+ if (audit.findings.length > 0) process.stderr.write(pc.yellow("Rewrite those descriptions in your own words before publishing.\n"));
140
+ }
141
+ const document = format === "html" ? statement.html : statement.markdown;
142
+ if (options.output) {
143
+ const target = path.resolve(options.cwd ?? process.cwd(), options.output);
144
+ await mkdir(path.dirname(target), { recursive: true });
145
+ await writeFile(target, document, "utf8");
146
+ process.stderr.write(pc.dim(`Written to ${options.output}\n`));
147
+ } else process.stdout.write(document);
148
+ process.stderr.write(pc.yellow("Review before publishing. This is a draft, not legal advice.\n"));
149
+ return {
150
+ document,
151
+ format,
152
+ exitCode: 0
153
+ };
154
+ } catch (cause) {
155
+ if (cause instanceof ConfigError || cause instanceof StatementError) {
156
+ process.stderr.write(`${pc.red("error")} ${cause.message}\n`);
157
+ if (cause instanceof ConfigError) for (const issue of cause.issues) process.stderr.write(pc.dim(` ${issue}\n`));
158
+ return {
159
+ document: "",
160
+ format,
161
+ exitCode: 2
162
+ };
163
+ }
164
+ throw cause;
165
+ }
166
+ }
167
+ /**
168
+ * `--output a11y.html` means HTML. Writing a full HTML document into a file
169
+ * somebody named .html and then having it be markdown would be a worse surprise
170
+ * than ignoring the extension, and `--format` still overrides this.
171
+ */
172
+ function formatFor(output) {
173
+ return output && /\.html?$/i.test(output) ? "html" : "markdown";
174
+ }
175
+ //#endregion
176
+ //#region src/cli/index.ts
177
+ const program = new Command();
178
+ program.exitOverride();
179
+ program.name("eaa-kit").description("WCAG 2.2 AA auditor and EU accessibility statement generator for static sites.\nNot legal advice.").version(TOOL_VERSION, "-v, --version");
180
+ program.command("audit").description("Audit built HTML against WCAG 2.2 AA").argument("[dir]", "directory holding the built site", "./dist").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--fail-on <impact>", `exit 1 on violations at or above this impact (${IMPACT_LEVELS.join("|")})`, parseImpact, DEFAULT_FAIL_ON).option("--format <format>", `output format (${OUTPUT_FORMATS.join("|")})`, parseFormat, "console").option("--output <path>", "write the report to a file instead of stdout").option("--browser", "audit in real Chromium, covering the rules jsdom cannot evaluate").option("--concurrency <n>", "worker threads to audit with, or 1 for none (default: from the page and core count)", parseConcurrency).option("--baseline <path>", "accept the violations recorded in this file; fail only on new ones").action(async (dir, options) => {
181
+ const { exitCode } = await runAuditCommand(dir, {
182
+ ...Array.isArray(options["include"]) ? { include: options["include"] } : {},
183
+ ...Array.isArray(options["exclude"]) ? { exclude: options["exclude"] } : {},
184
+ ...typeof options["baseUrl"] === "string" ? { baseUrl: options["baseUrl"] } : {},
185
+ failOn: options["failOn"],
186
+ format: options["format"],
187
+ ...typeof options["output"] === "string" ? { output: options["output"] } : {},
188
+ ...options["browser"] === true ? { browser: true } : {},
189
+ ...typeof options["concurrency"] === "number" ? { concurrency: options["concurrency"] } : {},
190
+ ...typeof options["baseline"] === "string" ? { baseline: options["baseline"] } : {}
191
+ });
192
+ process.exitCode = exitCode;
193
+ });
194
+ program.command("baseline").description("Record the violations a build already has, so later runs fail only on new ones").argument("[dir]", "directory holding the built site", "./dist").option("--include <globs...>", "glob patterns to audit, relative to dir").option("--exclude <globs...>", "glob patterns to skip").option("--base-url <url>", "audit pages under their real site URL").option("--output <path>", `where to write it (default: ${DEFAULT_BASELINE_FILE})`).option("--note <text>", "recorded on every entry, for whoever reads the file").option("--expires-on <date>", "ISO date after which the entries stop suppressing", parseDate).option("--browser", "audit in real Chromium instead of jsdom").option("--concurrency <n>", "worker threads to audit with, or 1 for none", parseConcurrency).action(async (dir, options) => {
195
+ const { exitCode } = await runBaselineCommand(dir, {
196
+ ...Array.isArray(options["include"]) ? { include: options["include"] } : {},
197
+ ...Array.isArray(options["exclude"]) ? { exclude: options["exclude"] } : {},
198
+ ...typeof options["baseUrl"] === "string" ? { baseUrl: options["baseUrl"] } : {},
199
+ ...typeof options["output"] === "string" ? { output: options["output"] } : {},
200
+ ...typeof options["note"] === "string" ? { note: options["note"] } : {},
201
+ ...typeof options["expiresOn"] === "string" ? { expiresOn: options["expiresOn"] } : {},
202
+ ...options["browser"] === true ? { browser: true } : {},
203
+ ...typeof options["concurrency"] === "number" ? { concurrency: options["concurrency"] } : {}
204
+ });
205
+ process.exitCode = exitCode;
206
+ });
207
+ program.command("statement").description("Generate an EU accessibility statement from eaa.config").option("--config <path>", "path to the config file, otherwise it is searched for").option("--lang <locale>", `statement language (${STATEMENT_LOCALES.join("|")})`, parseLocale).option("--country <code>", `override the country template (${COUNTRIES.join("|")})`, parseCountry).option("--audit <path>", "list the barriers from an eaa-kit audit --format json report").option("--format <format>", `output format (${STATEMENT_FORMATS.join("|")}), otherwise from the --output extension`, parseStatementFormat).option("--output <path>", "write the statement to a file instead of stdout").action(async (options) => {
208
+ const { exitCode } = await runStatementCommand({
209
+ ...typeof options["config"] === "string" ? { config: options["config"] } : {},
210
+ ...options["lang"] ? { locale: options["lang"] } : {},
211
+ ...options["country"] ? { country: options["country"] } : {},
212
+ ...typeof options["audit"] === "string" ? { audit: options["audit"] } : {},
213
+ ...options["format"] ? { format: options["format"] } : {},
214
+ ...typeof options["output"] === "string" ? { output: options["output"] } : {}
215
+ });
216
+ process.exitCode = exitCode;
217
+ });
218
+ function parseLocale(value) {
219
+ if (!STATEMENT_LOCALES.includes(value)) throw new InvalidArgumentError(`expected one of ${STATEMENT_LOCALES.join(", ")}`);
220
+ return value;
221
+ }
222
+ function parseCountry(value) {
223
+ const upper = value.toUpperCase();
224
+ if (!COUNTRIES.includes(upper)) throw new InvalidArgumentError(`expected one of ${COUNTRIES.join(", ")}`);
225
+ return upper;
226
+ }
227
+ function parseStatementFormat(value) {
228
+ if (!isStatementFormat(value)) throw new InvalidArgumentError(`expected one of ${STATEMENT_FORMATS.join(", ")}`);
229
+ return value;
230
+ }
231
+ function parseDate(value) {
232
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(Date.parse(`${value}T00:00:00Z`))) throw new InvalidArgumentError("expected an ISO date, e.g. 2026-12-31");
233
+ return value;
234
+ }
235
+ function parseConcurrency(value) {
236
+ const parsed = Number(value);
237
+ if (!Number.isInteger(parsed) || parsed < 1) throw new InvalidArgumentError("expected a whole number of 1 or more");
238
+ return parsed;
239
+ }
240
+ function parseImpact(value) {
241
+ if (!isImpactLevel(value)) throw new InvalidArgumentError(`expected one of ${IMPACT_LEVELS.join(", ")}`);
242
+ return value;
243
+ }
244
+ function parseFormat(value) {
245
+ if (!isOutputFormat(value)) throw new InvalidArgumentError(`expected one of ${OUTPUT_FORMATS.join(", ")}`);
246
+ return value;
247
+ }
248
+ try {
249
+ await program.parseAsync(process.argv);
250
+ } catch (cause) {
251
+ const error = cause;
252
+ if (typeof error.exitCode === "number") process.exitCode = error.exitCode === 0 ? 0 : 2;
253
+ else {
254
+ process.stderr.write(`${cause instanceof Error ? cause.stack : String(cause)}\n`);
255
+ process.exitCode = 2;
256
+ }
257
+ }
258
+ //#endregion
259
+ export {};
@@ -0,0 +1,21 @@
1
+ //#region src/escape.ts
2
+ /**
3
+ * HTML escaping, shared by everything in this package that writes HTML.
4
+ *
5
+ * Both documents eaa-kit produces embed text it did not write: an issue
6
+ * description from a config file, axe-core's help text, and — in the audit
7
+ * report — the markup of the element that failed, which is by definition
8
+ * arbitrary HTML from somebody's build. Getting this wrong in the report would
9
+ * mean a page that fails an accessibility audit for having a stray `<script>`
10
+ * hands that script to whoever opens the report.
11
+ */
12
+ /** For text nodes. Leaves quotes alone, which are fine between tags. */
13
+ function escapeText(value) {
14
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
15
+ }
16
+ /** For attribute values, where a quote would end the attribute. */
17
+ function escapeAttribute(value) {
18
+ return escapeText(value).replace(/"/g, "&quot;").replace(/'/g, "&#39;");
19
+ }
20
+ //#endregion
21
+ export { escapeText as n, escapeAttribute as t };
@@ -0,0 +1,20 @@
1
+ import { createHash } from "node:crypto";
2
+ //#region src/audit/fingerprint.ts
3
+ /**
4
+ * A stable identity for one violating element.
5
+ *
6
+ * Derived from the rule, the selector and the element's own markup, and
7
+ * deliberately not from the file it was found in. Two consumers need this and
8
+ * they need it to agree: SARIF, so that moving a page does not close one code
9
+ * scanning alert and open an identical one, and the baseline, so that an
10
+ * accepted violation stays accepted when the surrounding page changes.
11
+ *
12
+ * Sixteen hex characters. This identifies a defect for humans and tooling, not
13
+ * a secret, and a full digest in every entry would make a baseline file for a
14
+ * large site unreadable.
15
+ */
16
+ function elementFingerprint(ruleId, selector, html) {
17
+ return createHash("sha256").update(`${ruleId}\n${selector}\n${html}`).digest("hex").slice(0, 16);
18
+ }
19
+ //#endregion
20
+ export { elementFingerprint as t };