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.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/astro/index.d.ts +90 -0
- package/dist/astro/index.js +48 -0
- package/dist/audit/runners/worker.d.ts +1 -0
- package/dist/audit/runners/worker.js +27 -0
- package/dist/audit-6gbV0Zjd.js +582 -0
- package/dist/audit-VtuUOuyX.js +2 -0
- package/dist/baseline-DQTnNlc4.js +169 -0
- package/dist/baseline-Itspu3-Y.js +2 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +259 -0
- package/dist/escape-Dm1o_RAk.js +21 -0
- package/dist/fingerprint-DRoneAjj.js +20 -0
- package/dist/html-BLEuzep6.js +337 -0
- package/dist/impact-DvgBjupx.js +32 -0
- package/dist/impact-EEB9ZXmC.d.ts +7 -0
- package/dist/index.d.ts +264 -0
- package/dist/index.js +2 -0
- package/dist/jsdom-BEu6Ra_2.js +163 -0
- package/dist/jsdom-C6dIyaxN.js +3 -0
- package/dist/json-1ESNIiHY.js +139 -0
- package/dist/playwright-BfWuTG_u.js +235 -0
- package/dist/pool-DixLeu8L.js +188 -0
- package/dist/render-K9KxDDSA.js +774 -0
- package/dist/result-2aZPfM8w.js +168 -0
- package/dist/sarif-eSCuI0eX.js +192 -0
- package/dist/statement/templates/at.de.md +127 -0
- package/dist/statement/templates/at.en.md +121 -0
- package/dist/statement/templates/ch.de.md +138 -0
- package/dist/statement/templates/ch.en.md +135 -0
- package/dist/statement/templates/de.de.md +129 -0
- package/dist/statement/templates/de.en.md +125 -0
- package/dist/version-B3v4rNoG.js +15 -0
- package/package.json +95 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { i as isImpactLevel, r as countAtOrAbove } from "./impact-DvgBjupx.js";
|
|
2
|
+
import { n as escapeText, t as escapeAttribute } from "./escape-Dm1o_RAk.js";
|
|
3
|
+
import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
|
|
4
|
+
import axe from "axe-core";
|
|
5
|
+
//#region src/audit/report/html.ts
|
|
6
|
+
/**
|
|
7
|
+
* A standalone HTML audit report.
|
|
8
|
+
*
|
|
9
|
+
* The console report is for the person who ran the command; JSON and SARIF are
|
|
10
|
+
* for other programs. This one is for somebody who was not at the terminal —
|
|
11
|
+
* the client whose site it is, or the colleague who has to fix it — so it is a
|
|
12
|
+
* single file that can be attached to an email and opened, with no server, no
|
|
13
|
+
* assets and no scripts.
|
|
14
|
+
*
|
|
15
|
+
* It says exactly what the console report says, in the same order and with the
|
|
16
|
+
* same refusals: the four result categories stay apart, rules this engine could
|
|
17
|
+
* not evaluate are named rather than quietly dropped, and nothing here adds up
|
|
18
|
+
* to a compliance claim. A report with no findings means no findings were
|
|
19
|
+
* found, which is not the same as a site being accessible, and the document
|
|
20
|
+
* says so in its own footer rather than leaving the reader to infer it.
|
|
21
|
+
*/
|
|
22
|
+
/** Elements listed per rule before the rest are summarised. */
|
|
23
|
+
const MAX_NODES = 5;
|
|
24
|
+
/** Longest element markup shown before it is truncated. */
|
|
25
|
+
const MAX_SNIPPET = 200;
|
|
26
|
+
function buildHtmlReport(audits, options) {
|
|
27
|
+
const engine = audits[0]?.engine ?? "jsdom";
|
|
28
|
+
const failing = countAtOrAbove(audits, options.failOn);
|
|
29
|
+
const generatedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
30
|
+
const title = `Accessibility audit · ${options.directory}`;
|
|
31
|
+
return `<!doctype html>
|
|
32
|
+
<html lang="en">
|
|
33
|
+
<head>
|
|
34
|
+
<meta charset="utf-8">
|
|
35
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
36
|
+
<meta name="generator" content="eaa-kit ${escapeAttribute(TOOL_VERSION)}">
|
|
37
|
+
<title>${escapeText(title)}</title>
|
|
38
|
+
<style>
|
|
39
|
+
${STYLES}
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<main>
|
|
44
|
+
<h1>Accessibility audit</h1>
|
|
45
|
+
${verdict(audits, failing, options)}
|
|
46
|
+
${runDetails(audits, engine, generatedAt, options)}
|
|
47
|
+
${summary(audits, failing, options)}
|
|
48
|
+
${pages(audits)}
|
|
49
|
+
${notEvaluated(audits)}
|
|
50
|
+
${footer()}
|
|
51
|
+
</main>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The result, in words, before any of the detail.
|
|
58
|
+
*
|
|
59
|
+
* Not colour alone: the badge carries the word as well, because a reader who
|
|
60
|
+
* cannot distinguish the two shades still has to be able to read the outcome —
|
|
61
|
+
* which would be an embarrassing thing for this document in particular to get
|
|
62
|
+
* wrong.
|
|
63
|
+
*/
|
|
64
|
+
function verdict(audits, failing, options) {
|
|
65
|
+
const unaudited = audits.filter((audit) => audit.error).length;
|
|
66
|
+
if (unaudited > 0) return banner("broken", "Could not finish", `${count(unaudited, "page")} could not be audited, so this run reached no verdict.`);
|
|
67
|
+
if (failing > 0) return banner("fail", "Violations found", `${count(failing, "violation")} at or above ${escapeText(options.failOn)}.`);
|
|
68
|
+
const below = totalViolations(audits) - failing;
|
|
69
|
+
if (below > 0) return banner("pass", "No violations at the threshold", `${count(below, "violation")} below ${escapeText(options.failOn)}, which do not fail the run.`);
|
|
70
|
+
return banner("pass", "No violations found", "Automated testing found nothing to report.");
|
|
71
|
+
}
|
|
72
|
+
function banner(kind, heading, detail) {
|
|
73
|
+
return `<p class="verdict ${escapeAttribute(kind)}"><strong>${escapeText(heading)}</strong> ${detail}</p>`;
|
|
74
|
+
}
|
|
75
|
+
function runDetails(audits, engine, generatedAt, options) {
|
|
76
|
+
const rows = [
|
|
77
|
+
["Directory", options.directory],
|
|
78
|
+
["Pages", String(audits.length)],
|
|
79
|
+
["Engine", engine === "browser" ? "Chromium" : "jsdom (browserless)"],
|
|
80
|
+
["Threshold", `${options.failOn} and above fails the run`],
|
|
81
|
+
["Generated", generatedAt],
|
|
82
|
+
["eaa-kit", `${TOOL_VERSION} · axe-core ${axe.version}`]
|
|
83
|
+
];
|
|
84
|
+
if (options.baseUrl) rows.splice(1, 0, ["Base URL", options.baseUrl]);
|
|
85
|
+
return `<h2>The run</h2>\n<dl class="run">\n${rows.map(([term, value]) => ` <div><dt>${escapeText(term)}</dt><dd>${escapeText(value)}</dd></div>`).join("\n")}\n</dl>`;
|
|
86
|
+
}
|
|
87
|
+
function summary(audits, failing, options) {
|
|
88
|
+
const byImpact = /* @__PURE__ */ new Map();
|
|
89
|
+
let needsReview = 0;
|
|
90
|
+
let blind = 0;
|
|
91
|
+
let passes = 0;
|
|
92
|
+
let inapplicable = 0;
|
|
93
|
+
let elements = 0;
|
|
94
|
+
let accepted = 0;
|
|
95
|
+
for (const audit of audits) {
|
|
96
|
+
for (const finding of audit.accepted ?? []) accepted += finding.nodes.length;
|
|
97
|
+
for (const finding of audit.violations) {
|
|
98
|
+
const impact = finding.impact && isImpactLevel(finding.impact) ? finding.impact : "unclassified";
|
|
99
|
+
byImpact.set(impact, (byImpact.get(impact) ?? 0) + 1);
|
|
100
|
+
elements += finding.nodes.length;
|
|
101
|
+
}
|
|
102
|
+
for (const finding of audit.incomplete) if (finding.reason === "engine-limitation") blind += 1;
|
|
103
|
+
else needsReview += 1;
|
|
104
|
+
passes += audit.passes.length;
|
|
105
|
+
inapplicable += audit.inapplicable.length;
|
|
106
|
+
}
|
|
107
|
+
const impacts = [
|
|
108
|
+
"critical",
|
|
109
|
+
"serious",
|
|
110
|
+
"moderate",
|
|
111
|
+
"minor",
|
|
112
|
+
"unclassified"
|
|
113
|
+
].filter((impact) => (byImpact.get(impact) ?? 0) > 0).map((impact) => ` <li><span class="badge ${escapeAttribute(impact)}">${escapeText(impact)}</span> ${byImpact.get(impact)}</li>`).join("\n");
|
|
114
|
+
const withViolations = audits.filter((audit) => audit.violations.length > 0).length;
|
|
115
|
+
return `<h2>Summary</h2>
|
|
116
|
+
<ul class="counts">
|
|
117
|
+
<li><strong>${count(totalViolations(audits), "violation")}</strong> on ${withViolations} of ${count(audits.length, "page")}, across ${count(elements, "element")}</li>
|
|
118
|
+
<li><strong>${failing}</strong> at or above ${escapeText(options.failOn)}</li>
|
|
119
|
+
<li><strong>${needsReview}</strong> ${needsReview === 1 ? "rule needs" : "rules need"} manual review</li>
|
|
120
|
+
<li><strong>${blind}</strong> ${blind === 1 ? "rule was" : "rules were"} not evaluated by this engine</li>
|
|
121
|
+
${accepted > 0 ? ` <li><strong>${accepted}</strong> ${accepted === 1 ? "element is" : "elements are"} accepted by the baseline, and not counted above</li>` : ""}
|
|
122
|
+
</ul>
|
|
123
|
+
${impacts ? `<ul class="impacts">\n${impacts}\n</ul>` : ""}
|
|
124
|
+
<p class="note">
|
|
125
|
+
<strong>${passes}</strong> rule results were checked and met, and <strong>${inapplicable}</strong>
|
|
126
|
+
found nothing on the page to check. Those two are counted separately and never added
|
|
127
|
+
together: a rule with nothing to check is not a rule that passed, and a page with no
|
|
128
|
+
images proves nothing about image alternatives.
|
|
129
|
+
</p>`;
|
|
130
|
+
}
|
|
131
|
+
function pages(audits) {
|
|
132
|
+
return `<h2>Pages</h2>\n${audits.map(pageSection).join("\n")}`;
|
|
133
|
+
}
|
|
134
|
+
function pageSection(audit) {
|
|
135
|
+
const heading = `<h3 class="page">${escapeText(audit.relativePath)}</h3>`;
|
|
136
|
+
if (audit.error) return `${heading}
|
|
137
|
+
<p class="verdict broken"><strong>Not audited</strong> ${escapeText(audit.error)}</p>`;
|
|
138
|
+
const parts = [heading];
|
|
139
|
+
if (audit.violations.length === 0) parts.push((audit.accepted ?? []).length === 0 ? "<p class=\"clean\">No violations.</p>" : "<p class=\"coverage\">No new violations.</p>");
|
|
140
|
+
else {
|
|
141
|
+
const findings = [...audit.violations].sort(byImpactThenRule).map(violation).join("\n");
|
|
142
|
+
parts.push(`<ol class="findings">\n${findings}\n</ol>`);
|
|
143
|
+
}
|
|
144
|
+
const review = audit.incomplete.filter((finding) => finding.reason === "needs-review");
|
|
145
|
+
if (review.length > 0) {
|
|
146
|
+
const items = review.map((finding) => ` <li><code>${escapeText(finding.ruleId)}</code> needs manual review${standards(finding)}</li>`).join("\n");
|
|
147
|
+
parts.push(`<p class="review-heading">A human has to decide these:</p>\n<ul>\n${items}\n</ul>`);
|
|
148
|
+
}
|
|
149
|
+
const accepted = audit.accepted ?? [];
|
|
150
|
+
if (accepted.length > 0) {
|
|
151
|
+
const items = [...accepted].sort(byImpactThenRule).map((finding) => ` <li><code>${escapeText(finding.ruleId)}</code> — ${escapeText(finding.help)} (${count(finding.nodes.length, "element")})</li>`).join("\n");
|
|
152
|
+
parts.push(`<p class="accepted-heading">Accepted by the baseline. Still violations, and not counted above:</p>\n<ul class="accepted">\n${items}\n</ul>`);
|
|
153
|
+
}
|
|
154
|
+
parts.push(coverage(audit));
|
|
155
|
+
return parts.join("\n");
|
|
156
|
+
}
|
|
157
|
+
function violation(finding) {
|
|
158
|
+
const impact = finding.impact && isImpactLevel(finding.impact) ? finding.impact : "unclassified";
|
|
159
|
+
const shown = finding.nodes.slice(0, MAX_NODES);
|
|
160
|
+
const remaining = finding.nodes.length - shown.length;
|
|
161
|
+
const nodes = shown.map((node) => ` <li>
|
|
162
|
+
<code class="selector">${escapeText(node.target.join(" "))}</code>
|
|
163
|
+
<pre><code>${escapeText(snippet(node.html))}</code></pre>
|
|
164
|
+
</li>`).join("\n");
|
|
165
|
+
const more = remaining > 0 ? `\n <li class="more">and ${count(remaining, "more element")}</li>` : "";
|
|
166
|
+
return ` <li class="finding">
|
|
167
|
+
<p class="rule">
|
|
168
|
+
<span class="badge ${escapeAttribute(impact)}">${escapeText(impact)}</span>
|
|
169
|
+
<code>${escapeText(finding.ruleId)}</code>
|
|
170
|
+
<a href="${escapeAttribute(finding.helpUrl)}">${escapeText(finding.help)}</a>
|
|
171
|
+
</p>
|
|
172
|
+
<p class="standards">${standardsText(finding) || "No mapped success criterion"}</p>
|
|
173
|
+
<ul class="nodes">
|
|
174
|
+
${nodes}${more}
|
|
175
|
+
</ul>
|
|
176
|
+
</li>`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* What this page's result rests on.
|
|
180
|
+
*
|
|
181
|
+
* The four counts stay apart for the same reason they do everywhere else: only
|
|
182
|
+
* `passed` is evidence that anything was met here.
|
|
183
|
+
*/
|
|
184
|
+
function coverage(audit) {
|
|
185
|
+
const blind = audit.incomplete.filter((finding) => finding.reason === "engine-limitation").length;
|
|
186
|
+
const review = audit.incomplete.length - blind;
|
|
187
|
+
const parts = [`${audit.passes.length} passed`, `${audit.inapplicable.length} not applicable`];
|
|
188
|
+
if (review > 0) parts.push(`${review} to review`);
|
|
189
|
+
if (blind > 0) parts.push(`${blind} not evaluated`);
|
|
190
|
+
return `<p class="coverage">${escapeText(parts.join(" · "))}</p>`;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Rules this engine reached no verdict on, listed once at the end.
|
|
194
|
+
*
|
|
195
|
+
* They are never reported as passing, and leaving them out entirely would let
|
|
196
|
+
* the reader take the rest of the document for full coverage.
|
|
197
|
+
*/
|
|
198
|
+
function notEvaluated(audits) {
|
|
199
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
200
|
+
for (const audit of audits) for (const finding of audit.incomplete) {
|
|
201
|
+
if (finding.reason !== "engine-limitation") continue;
|
|
202
|
+
const existing = byRule.get(finding.ruleId);
|
|
203
|
+
if (existing) existing.pages += 1;
|
|
204
|
+
else byRule.set(finding.ruleId, {
|
|
205
|
+
pages: 1,
|
|
206
|
+
detail: finding.reasonDetail,
|
|
207
|
+
finding
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (byRule.size === 0) return "";
|
|
211
|
+
return `<h2>Not evaluated</h2>
|
|
212
|
+
<p>This engine reached no verdict on these rules. They are never reported as passing.</p>
|
|
213
|
+
<ul class="not-evaluated">
|
|
214
|
+
${[...byRule.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ruleId, entry]) => ` <li>
|
|
215
|
+
<code>${escapeText(ruleId)}</code> on ${count(entry.pages, "page")}${standards(entry.finding)}
|
|
216
|
+
<span class="reason">${escapeText(entry.detail)}</span>
|
|
217
|
+
</li>`).join("\n")}
|
|
218
|
+
</ul>`;
|
|
219
|
+
}
|
|
220
|
+
function footer() {
|
|
221
|
+
return `<hr>
|
|
222
|
+
<footer>
|
|
223
|
+
<p>
|
|
224
|
+
Generated with <a href="https://github.com/likeBloodMoon/eaa-kit">eaa-kit</a>. Automated
|
|
225
|
+
testing finds a minority of accessibility barriers: it cannot judge whether alternative
|
|
226
|
+
text is accurate, whether a page makes sense in reading order, or whether a form can
|
|
227
|
+
actually be completed with a screen reader.
|
|
228
|
+
</p>
|
|
229
|
+
<p>
|
|
230
|
+
<strong>A report with no findings is not a compliance statement.</strong> It means
|
|
231
|
+
nothing was found by this engine, which is not the same as a site being accessible.
|
|
232
|
+
</p>
|
|
233
|
+
</footer>`;
|
|
234
|
+
}
|
|
235
|
+
function standards(finding) {
|
|
236
|
+
const text = standardsText(finding);
|
|
237
|
+
return text ? ` — ${text}` : "";
|
|
238
|
+
}
|
|
239
|
+
function standardsText(finding) {
|
|
240
|
+
const parts = [...finding.successCriteria.map((criterion) => `WCAG ${criterion}`), ...finding.enClauses.map((clause) => `EN 301 549 ${clause}`)];
|
|
241
|
+
return escapeText(parts.join(", "));
|
|
242
|
+
}
|
|
243
|
+
/** Element markup, on one line and bounded, so one minified page cannot fill the report. */
|
|
244
|
+
function snippet(html) {
|
|
245
|
+
const collapsed = html.replace(/\s+/g, " ").trim();
|
|
246
|
+
return collapsed.length > MAX_SNIPPET ? `${collapsed.slice(0, 199)}…` : collapsed;
|
|
247
|
+
}
|
|
248
|
+
function totalViolations(audits) {
|
|
249
|
+
return audits.reduce((total, audit) => total + audit.violations.length, 0);
|
|
250
|
+
}
|
|
251
|
+
function byImpactThenRule(a, b) {
|
|
252
|
+
const order = [
|
|
253
|
+
"critical",
|
|
254
|
+
"serious",
|
|
255
|
+
"moderate",
|
|
256
|
+
"minor"
|
|
257
|
+
];
|
|
258
|
+
const rank = (finding) => {
|
|
259
|
+
const index = finding.impact ? order.indexOf(finding.impact) : -1;
|
|
260
|
+
return index === -1 ? -1 : index;
|
|
261
|
+
};
|
|
262
|
+
const difference = rank(a) - rank(b);
|
|
263
|
+
return difference === 0 ? a.ruleId.localeCompare(b.ruleId) : difference;
|
|
264
|
+
}
|
|
265
|
+
function count(value, noun) {
|
|
266
|
+
return `${value} ${noun}${value === 1 ? "" : "s"}`;
|
|
267
|
+
}
|
|
268
|
+
const STYLES = `:root { color-scheme: light dark; }
|
|
269
|
+
body { margin: 0; background: #ffffff; color: #1a1a1a;
|
|
270
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; line-height: 1.6; }
|
|
271
|
+
main { max-width: 60rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
|
272
|
+
h1 { font-size: 1.9rem; line-height: 1.25; margin: 0 0 1rem; }
|
|
273
|
+
h2 { font-size: 1.35rem; margin: 2.5rem 0 0.75rem; padding-top: 1.5rem;
|
|
274
|
+
border-top: 1px solid #d4d4d4; }
|
|
275
|
+
h3.page { font-size: 1.05rem; margin: 2rem 0 0.5rem; font-family: ui-monospace, monospace; }
|
|
276
|
+
p { margin: 0 0 1rem; }
|
|
277
|
+
a { color: #0b4fa8; }
|
|
278
|
+
a:focus-visible { outline: 3px solid currentColor; outline-offset: 2px; }
|
|
279
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; }
|
|
280
|
+
pre { margin: 0.4rem 0 0; padding: 0.6rem 0.75rem; overflow-x: auto;
|
|
281
|
+
background: #f4f4f5; border-radius: 4px; }
|
|
282
|
+
pre code { font-size: 0.85em; }
|
|
283
|
+
ul, ol { margin: 0 0 1rem; padding-left: 1.25rem; }
|
|
284
|
+
li + li { margin-top: 0.5rem; }
|
|
285
|
+
.verdict { padding: 0.75rem 1rem; border-radius: 4px; border-left: 4px solid; }
|
|
286
|
+
.verdict.pass { background: #eef7ee; border-color: #216e39; }
|
|
287
|
+
.verdict.fail { background: #fdeeee; border-color: #a01b1b; }
|
|
288
|
+
.verdict.broken { background: #fdf4e3; border-color: #8a5a00; }
|
|
289
|
+
.badge { display: inline-block; padding: 0 0.45rem; border-radius: 3px; font-size: 0.8rem;
|
|
290
|
+
font-weight: 600; border: 1px solid; }
|
|
291
|
+
.badge.critical { background: #fdeeee; border-color: #a01b1b; color: #7a1414; }
|
|
292
|
+
.badge.serious { background: #fdf1e8; border-color: #a4531b; color: #7d3f14; }
|
|
293
|
+
.badge.moderate { background: #fdf9e3; border-color: #7a6100; color: #5c4900; }
|
|
294
|
+
.badge.minor { background: #eef2f8; border-color: #40556f; color: #33455a; }
|
|
295
|
+
.badge.unclassified { background: #f1f1f1; border-color: #565656; color: #444444; }
|
|
296
|
+
dl.run { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1.5rem; margin: 0; }
|
|
297
|
+
dl.run > div { display: contents; }
|
|
298
|
+
dl.run dt { font-weight: 600; }
|
|
299
|
+
dl.run dd { margin: 0; }
|
|
300
|
+
ul.counts, ul.impacts { list-style: none; padding-left: 0; }
|
|
301
|
+
ul.impacts li { display: inline-block; margin-right: 1rem; }
|
|
302
|
+
ol.findings { list-style: none; padding-left: 0; }
|
|
303
|
+
li.finding { margin: 0 0 1.5rem; padding-left: 0.9rem; border-left: 3px solid #d4d4d4; }
|
|
304
|
+
p.rule { margin-bottom: 0.25rem; }
|
|
305
|
+
p.standards, p.coverage, .reason, li.more, p.accepted-heading, ul.accepted { color: #4a4a4a; font-size: 0.9rem; }
|
|
306
|
+
p.coverage { margin-top: 0.5rem; }
|
|
307
|
+
ul.nodes { list-style: none; padding-left: 0; }
|
|
308
|
+
code.selector { color: #4a4a4a; }
|
|
309
|
+
p.clean { color: #216e39; }
|
|
310
|
+
p.note { font-size: 0.95rem; }
|
|
311
|
+
hr { border: 0; border-top: 1px solid #d4d4d4; margin: 3rem 0 1.5rem; }
|
|
312
|
+
footer { color: #4a4a4a; font-size: 0.9rem; }
|
|
313
|
+
@media (prefers-color-scheme: dark) {
|
|
314
|
+
body { background: #121212; color: #ededed; }
|
|
315
|
+
a { color: #9ec1ff; }
|
|
316
|
+
h2, hr { border-color: #3a3a3a; }
|
|
317
|
+
pre { background: #1e1e1e; }
|
|
318
|
+
li.finding { border-color: #3a3a3a; }
|
|
319
|
+
p.standards, p.coverage, .reason, li.more, code.selector, footer,
|
|
320
|
+
p.accepted-heading, ul.accepted { color: #b6b6b6; }
|
|
321
|
+
p.clean { color: #7ee2a8; }
|
|
322
|
+
.verdict.pass { background: #10240f; border-color: #7ee2a8; }
|
|
323
|
+
.verdict.fail { background: #2b1111; border-color: #ff9d9d; }
|
|
324
|
+
.verdict.broken { background: #2b2310; border-color: #ffd28a; }
|
|
325
|
+
.badge.critical { background: #2b1111; border-color: #ff9d9d; color: #ffc9c9; }
|
|
326
|
+
.badge.serious { background: #2b1c11; border-color: #ffbb8a; color: #ffd9bd; }
|
|
327
|
+
.badge.moderate { background: #262110; border-color: #ffe08a; color: #ffeec2; }
|
|
328
|
+
.badge.minor { background: #16202b; border-color: #a8c6e8; color: #cfdff0; }
|
|
329
|
+
.badge.unclassified { background: #1f1f1f; border-color: #b6b6b6; color: #d8d8d8; }
|
|
330
|
+
}
|
|
331
|
+
@media print {
|
|
332
|
+
body { background: #ffffff; color: #000000; }
|
|
333
|
+
main { max-width: none; }
|
|
334
|
+
a { color: #000000; }
|
|
335
|
+
}`;
|
|
336
|
+
//#endregion
|
|
337
|
+
export { buildHtmlReport };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/audit/impact.ts
|
|
2
|
+
/** axe-core's impact scale, least to most severe. */
|
|
3
|
+
const IMPACT_LEVELS = [
|
|
4
|
+
"minor",
|
|
5
|
+
"moderate",
|
|
6
|
+
"serious",
|
|
7
|
+
"critical"
|
|
8
|
+
];
|
|
9
|
+
/** What `--fail-on` defaults to: serious and critical break the build. */
|
|
10
|
+
const DEFAULT_FAIL_ON = "serious";
|
|
11
|
+
function isImpactLevel(value) {
|
|
12
|
+
return IMPACT_LEVELS.includes(value);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether a violation is severe enough to fail the run.
|
|
16
|
+
*
|
|
17
|
+
* A violation whose impact axe-core did not classify counts at every threshold.
|
|
18
|
+
* Guessing low would let an unclassified failure through silently, and a
|
|
19
|
+
* missing impact is a gap in what we know, not evidence that it is harmless.
|
|
20
|
+
*/
|
|
21
|
+
function meetsThreshold(impact, threshold) {
|
|
22
|
+
if (impact === null || !isImpactLevel(impact)) return true;
|
|
23
|
+
return IMPACT_LEVELS.indexOf(impact) >= IMPACT_LEVELS.indexOf(threshold);
|
|
24
|
+
}
|
|
25
|
+
/** Violations at or above `threshold`, counted per rule per page. */
|
|
26
|
+
function countAtOrAbove(audits, threshold) {
|
|
27
|
+
let total = 0;
|
|
28
|
+
for (const audit of audits) for (const finding of audit.violations) if (meetsThreshold(finding.impact, threshold)) total += 1;
|
|
29
|
+
return total;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { isImpactLevel as i, IMPACT_LEVELS as n, countAtOrAbove as r, DEFAULT_FAIL_ON as t };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import "axe-core";
|
|
2
|
+
//#region src/audit/impact.d.ts
|
|
3
|
+
/** axe-core's impact scale, least to most severe. */
|
|
4
|
+
declare const IMPACT_LEVELS: readonly ['minor', 'moderate', 'serious', 'critical'];
|
|
5
|
+
type ImpactLevel = (typeof IMPACT_LEVELS)[number];
|
|
6
|
+
//#endregion
|
|
7
|
+
export { ImpactLevel as t };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { t as ImpactLevel } from "./impact-EEB9ZXmC.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/config/define.d.ts
|
|
4
|
+
/** Countries with their own supervisory body and statute text. */
|
|
5
|
+
declare const COUNTRIES: readonly ['AT', 'DE', 'CH'];
|
|
6
|
+
type Country = (typeof COUNTRIES)[number];
|
|
7
|
+
/** Languages a statement can be rendered in. */
|
|
8
|
+
declare const STATEMENT_LOCALES: readonly ['de', 'en'];
|
|
9
|
+
type StatementLocale = (typeof STATEMENT_LOCALES)[number];
|
|
10
|
+
/**
|
|
11
|
+
* Wording follows the EU model statement: fully, partially, or not conformant
|
|
12
|
+
* with the standard. "partially-compliant" is the honest answer for most sites
|
|
13
|
+
* and the one that carries obligations to list what is missing.
|
|
14
|
+
*/
|
|
15
|
+
declare const COMPLIANCE_STATUSES: readonly ['compliant', 'partially-compliant', 'non-compliant'];
|
|
16
|
+
type ComplianceStatus = (typeof COMPLIANCE_STATUSES)[number];
|
|
17
|
+
declare const ASSESSMENT_METHODS: readonly ['self-assessment', 'external-audit'];
|
|
18
|
+
type AssessmentMethod = (typeof ASSESSMENT_METHODS)[number];
|
|
19
|
+
/**
|
|
20
|
+
* Why a known barrier still exists. The first two are the grounds the EU regime
|
|
21
|
+
* recognises for leaving something inaccessible; the third is a plain promise to
|
|
22
|
+
* fix it, which is what most small sites actually mean.
|
|
23
|
+
*/
|
|
24
|
+
declare const ISSUE_REASONS: readonly ['disproportionate-burden', 'out-of-scope', 'fix-planned'];
|
|
25
|
+
type IssueReason = (typeof ISSUE_REASONS)[number];
|
|
26
|
+
declare const configSchema: z.ZodObject<{
|
|
27
|
+
site: z.ZodObject<{
|
|
28
|
+
name: z.ZodString;
|
|
29
|
+
url: z.ZodURL;
|
|
30
|
+
locale: z.ZodString;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
provider: z.ZodObject<{
|
|
33
|
+
legalName: z.ZodString;
|
|
34
|
+
email: z.ZodEmail;
|
|
35
|
+
phone: z.ZodOptional<z.ZodString>;
|
|
36
|
+
address: z.ZodOptional<z.ZodString>;
|
|
37
|
+
feedbackUrl: z.ZodOptional<z.ZodURL>;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
compliance: z.ZodObject<{
|
|
40
|
+
status: z.ZodEnum<{
|
|
41
|
+
compliant: "compliant";
|
|
42
|
+
"non-compliant": "non-compliant";
|
|
43
|
+
"partially-compliant": "partially-compliant";
|
|
44
|
+
}>;
|
|
45
|
+
standard: z.ZodDefault<z.ZodString>;
|
|
46
|
+
knownIssues: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<{
|
|
47
|
+
description: string;
|
|
48
|
+
}, string>>, z.ZodObject<{
|
|
49
|
+
description: z.ZodString;
|
|
50
|
+
successCriteria: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
51
|
+
en301549: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
52
|
+
reason: z.ZodOptional<z.ZodEnum<{
|
|
53
|
+
"disproportionate-burden": "disproportionate-burden";
|
|
54
|
+
"fix-planned": "fix-planned";
|
|
55
|
+
"out-of-scope": "out-of-scope";
|
|
56
|
+
}>>;
|
|
57
|
+
remedyBy: z.ZodOptional<z.ZodISODate>;
|
|
58
|
+
}, z.core.$strip>>, z.ZodObject<{
|
|
59
|
+
description: z.ZodString;
|
|
60
|
+
successCriteria: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
61
|
+
en301549: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
62
|
+
reason: z.ZodOptional<z.ZodEnum<{
|
|
63
|
+
"disproportionate-burden": "disproportionate-burden";
|
|
64
|
+
"fix-planned": "fix-planned";
|
|
65
|
+
"out-of-scope": "out-of-scope";
|
|
66
|
+
}>>;
|
|
67
|
+
remedyBy: z.ZodOptional<z.ZodISODate>;
|
|
68
|
+
}, z.core.$strip>]>>>;
|
|
69
|
+
assessedOn: z.ZodISODate;
|
|
70
|
+
assessmentMethod: z.ZodDefault<z.ZodEnum<{
|
|
71
|
+
"external-audit": "external-audit";
|
|
72
|
+
"self-assessment": "self-assessment";
|
|
73
|
+
}>>;
|
|
74
|
+
auditReason: z.ZodDefault<z.ZodEnum<{
|
|
75
|
+
"disproportionate-burden": "disproportionate-burden";
|
|
76
|
+
"fix-planned": "fix-planned";
|
|
77
|
+
"out-of-scope": "out-of-scope";
|
|
78
|
+
}>>;
|
|
79
|
+
}, z.core.$strip>;
|
|
80
|
+
enforcement: z.ZodObject<{
|
|
81
|
+
country: z.ZodEnum<{
|
|
82
|
+
AT: "AT";
|
|
83
|
+
CH: "CH";
|
|
84
|
+
DE: "DE";
|
|
85
|
+
}>;
|
|
86
|
+
}, z.core.$strip>;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
type EaaConfigInput = z.input<typeof configSchema>;
|
|
89
|
+
type EaaConfig = z.output<typeof configSchema>;
|
|
90
|
+
type KnownIssue = EaaConfig['compliance']['knownIssues'][number];
|
|
91
|
+
/**
|
|
92
|
+
* Identity function that gives `eaa.config.ts` its types. Deliberately does not
|
|
93
|
+
* validate: a config file is loaded and checked in one place, so that an error
|
|
94
|
+
* points at the file rather than at wherever the module happened to be
|
|
95
|
+
* imported.
|
|
96
|
+
*/
|
|
97
|
+
declare function defineConfig(config: EaaConfigInput): EaaConfigInput;
|
|
98
|
+
declare class ConfigError extends Error {
|
|
99
|
+
readonly issues: string[];
|
|
100
|
+
readonly name = "ConfigError";
|
|
101
|
+
constructor(message: string, issues?: string[]);
|
|
102
|
+
}
|
|
103
|
+
/** Validate an already-loaded config object. */
|
|
104
|
+
declare function parseConfig(value: unknown, source?: string): EaaConfig;
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/config/load.d.ts
|
|
107
|
+
/** Checked in this order, first match wins. */
|
|
108
|
+
declare const CONFIG_FILENAMES: readonly ['eaa.config.ts', 'eaa.config.mts', 'eaa.config.js', 'eaa.config.mjs', 'eaa.config.json'];
|
|
109
|
+
interface LoadedConfig {
|
|
110
|
+
config: EaaConfig;
|
|
111
|
+
/** Absolute path of the file it came from. */
|
|
112
|
+
path: string;
|
|
113
|
+
}
|
|
114
|
+
interface LoadConfigOptions {
|
|
115
|
+
/** Where to start looking. Defaults to the working directory. */
|
|
116
|
+
cwd?: string;
|
|
117
|
+
/** Explicit path, skipping the search. */
|
|
118
|
+
path?: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Find and load `eaa.config.{ts,mts,js,mjs,json}`.
|
|
122
|
+
*
|
|
123
|
+
* TypeScript configs are imported directly: Node strips types natively from
|
|
124
|
+
* 22.18 onwards, which is below this package's floor, so no bundler or loader
|
|
125
|
+
* dependency is needed. The failure mode that remains is a project with no
|
|
126
|
+
* package.json at all, where Node cannot tell ESM from CommonJS; the error says
|
|
127
|
+
* so rather than surfacing "Unexpected token 'export'".
|
|
128
|
+
*/
|
|
129
|
+
declare function loadConfig(options?: LoadConfigOptions): Promise<LoadedConfig>;
|
|
130
|
+
/** Walks up from `cwd`, so the CLI works from a subdirectory of the project. */
|
|
131
|
+
declare function findConfigFile(cwd: string): Promise<string | undefined>;
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/statement/error.d.ts
|
|
134
|
+
/**
|
|
135
|
+
* Lives in its own module so that both the renderer and the audit-report reader
|
|
136
|
+
* can throw it without importing each other.
|
|
137
|
+
*
|
|
138
|
+
* Everything the statement command can fail on lands here: a missing template,
|
|
139
|
+
* an audit report it cannot read. The CLI turns it into exit code 2, since a
|
|
140
|
+
* statement that could not be produced is never a statement with a problem in
|
|
141
|
+
* it — nothing is emitted at all.
|
|
142
|
+
*/
|
|
143
|
+
declare class StatementError extends Error {
|
|
144
|
+
readonly name = "StatementError";
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/statement/findings.d.ts
|
|
148
|
+
/** One rule that failed, folded across every page it failed on. */
|
|
149
|
+
interface AuditFinding {
|
|
150
|
+
ruleId: string;
|
|
151
|
+
/**
|
|
152
|
+
* axe-core's help text. English whatever the statement's language, which is
|
|
153
|
+
* why the templates label it as coming from the tool rather than presenting
|
|
154
|
+
* it as the provider's own description.
|
|
155
|
+
*/
|
|
156
|
+
help: string;
|
|
157
|
+
/** Least-to-most severe, or null when axe-core did not classify it. */
|
|
158
|
+
impact: ImpactLevel | null;
|
|
159
|
+
/** WCAG success criteria, e.g. ['1.1.1']. */
|
|
160
|
+
successCriteria: string[];
|
|
161
|
+
/** EN 301 549 clauses, e.g. ['9.1.1.1']. */
|
|
162
|
+
en301549: string[];
|
|
163
|
+
/** Pages it failed on, relative to the audited directory, sorted. */
|
|
164
|
+
pages: string[];
|
|
165
|
+
}
|
|
166
|
+
/** What an audit contributes to a statement. */
|
|
167
|
+
interface AuditSummary {
|
|
168
|
+
/** Violations, most severe first. */
|
|
169
|
+
findings: AuditFinding[];
|
|
170
|
+
/** Pages the run covered. */
|
|
171
|
+
pages: number;
|
|
172
|
+
/** Rules that need a human decision. Not barriers, and not evidence either. */
|
|
173
|
+
needsReview: number;
|
|
174
|
+
/** Rules the engine could not evaluate. Never reported as passing. */
|
|
175
|
+
notEvaluated: number;
|
|
176
|
+
/** When the audit ran. ISO 8601. */
|
|
177
|
+
generatedAt: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Turn a parsed `eaa-kit audit --format json` document into statement input.
|
|
181
|
+
*
|
|
182
|
+
* Only violations become barriers. A rule needing manual review has not been
|
|
183
|
+
* found inaccessible, and a rule the engine could not evaluate has not been
|
|
184
|
+
* found anything at all; listing either as non-accessible content would be a
|
|
185
|
+
* claim the audit never made. Both are carried as counts instead, so the
|
|
186
|
+
* statement can say how much the automated run left open.
|
|
187
|
+
*/
|
|
188
|
+
declare function summariseAuditReport(value: unknown, source?: string): AuditSummary;
|
|
189
|
+
/** Read and summarise a report written by `eaa-kit audit --format json`. */
|
|
190
|
+
declare function readAuditReport(file: string, cwd?: string): Promise<AuditSummary>;
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/statement/html.d.ts
|
|
193
|
+
/**
|
|
194
|
+
* Markdown-to-HTML for exactly the subset the statement templates emit, and
|
|
195
|
+
* nothing else.
|
|
196
|
+
*
|
|
197
|
+
* A general markdown parser is a dependency and a licence audit for a document
|
|
198
|
+
* whose entire vocabulary is four block types. What the templates produce:
|
|
199
|
+
*
|
|
200
|
+
* # heading level-1 and level-2 headings
|
|
201
|
+
* ## heading
|
|
202
|
+
* paragraph text soft-wrapped; joined back into one paragraph
|
|
203
|
+
* - list item with two-space continuation lines belonging to it
|
|
204
|
+
* --- a horizontal rule before the disclaimer
|
|
205
|
+
*
|
|
206
|
+
* Anything else is emitted as literal text, escaped. Emphasis, links, images
|
|
207
|
+
* and code spans are deliberately not implemented: a `**` in someone's issue
|
|
208
|
+
* description is far more likely to be a typo than a request for bold, and
|
|
209
|
+
* silently swallowing characters out of a legal document is the worse failure.
|
|
210
|
+
* Bare URLs and email addresses do become links, because they are unusable in
|
|
211
|
+
* an HTML document otherwise.
|
|
212
|
+
*/
|
|
213
|
+
interface HtmlDocumentOptions {
|
|
214
|
+
/**
|
|
215
|
+
* BCP 47 tag for the document text, e.g. 'de'. Becomes `<html lang>`, without
|
|
216
|
+
* which a screen reader announces German prose with an English voice.
|
|
217
|
+
*/
|
|
218
|
+
lang: string;
|
|
219
|
+
/** `<title>` when the markdown has no level-1 heading to take one from. */
|
|
220
|
+
fallbackTitle: string;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* A standalone, self-contained HTML page.
|
|
224
|
+
*
|
|
225
|
+
* Self-contained because the common destination is a CMS or a static host where
|
|
226
|
+
* a second file would have to be wired up by hand. The markup inside `<main>`
|
|
227
|
+
* carries no classes and no inline styles, so lifting it into an existing page
|
|
228
|
+
* template and dropping this one's `<style>` block is a copy and paste.
|
|
229
|
+
*/
|
|
230
|
+
declare function toHtmlDocument(markdown: string, options: HtmlDocumentOptions): string;
|
|
231
|
+
/** The document body: the block elements alone, for embedding in a page. */
|
|
232
|
+
declare function toHtmlBody(markdown: string): string;
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/statement/render.d.ts
|
|
235
|
+
interface RenderStatementOptions {
|
|
236
|
+
/** Language to render in. Defaults to the site's own language when it is German. */
|
|
237
|
+
locale?: StatementLocale;
|
|
238
|
+
/** Overrides the country from the config, for previewing another template. */
|
|
239
|
+
country?: Country;
|
|
240
|
+
/**
|
|
241
|
+
* Findings from `eaa-kit audit --format json`, appended to the barriers the
|
|
242
|
+
* config lists. Left out, the statement says only what the config says.
|
|
243
|
+
*/
|
|
244
|
+
audit?: AuditSummary;
|
|
245
|
+
}
|
|
246
|
+
interface RenderedStatement {
|
|
247
|
+
markdown: string;
|
|
248
|
+
/** The same document as a standalone HTML page. */
|
|
249
|
+
html: string;
|
|
250
|
+
locale: StatementLocale;
|
|
251
|
+
country: Country;
|
|
252
|
+
/** Template the text came from, e.g. 'at.de'. */
|
|
253
|
+
template: string;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Render an accessibility statement from a validated config.
|
|
257
|
+
*
|
|
258
|
+
* All prose lives in the templates. This function only decides which template
|
|
259
|
+
* to load and prepares the values it interpolates, including the booleans the
|
|
260
|
+
* template branches on, so that no German sentence is assembled in TypeScript.
|
|
261
|
+
*/
|
|
262
|
+
declare function renderStatement(config: EaaConfig, options?: RenderStatementOptions): Promise<RenderedStatement>;
|
|
263
|
+
//#endregion
|
|
264
|
+
export { ASSESSMENT_METHODS, type AssessmentMethod, type AuditFinding, type AuditSummary, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, type ComplianceStatus, ConfigError, type Country, type EaaConfig, type EaaConfigInput, type HtmlDocumentOptions, ISSUE_REASONS, type IssueReason, type KnownIssue, type LoadConfigOptions, type LoadedConfig, type RenderStatementOptions, type RenderedStatement, STATEMENT_LOCALES, StatementError, type StatementLocale, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { _ as defineConfig, a as summariseAuditReport, c as findConfigFile, d as COMPLIANCE_STATUSES, f as COUNTRIES, g as configSchema, h as STATEMENT_LOCALES, i as readAuditReport, l as loadConfig, m as ISSUE_REASONS, n as toHtmlBody, o as StatementError, p as ConfigError, r as toHtmlDocument, s as CONFIG_FILENAMES, t as renderStatement, u as ASSESSMENT_METHODS, v as parseConfig } from "./render-K9KxDDSA.js";
|
|
2
|
+
export { ASSESSMENT_METHODS, COMPLIANCE_STATUSES, CONFIG_FILENAMES, COUNTRIES, ConfigError, ISSUE_REASONS, STATEMENT_LOCALES, StatementError, configSchema, defineConfig, findConfigFile, loadConfig, parseConfig, readAuditReport, renderStatement, summariseAuditReport, toHtmlBody, toHtmlDocument };
|