eaa-kit 0.3.0 → 0.5.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 (66) hide show
  1. package/README.md +82 -14
  2. package/dist/astro/index.d.ts +1 -1
  3. package/dist/astro/index.js +1 -1
  4. package/dist/audit/runners/worker.js +5 -1
  5. package/dist/{audit-DcL73mOC.js → audit-B282A3EA.js} +207 -41
  6. package/dist/audit-DyHPmGeD.js +2 -0
  7. package/dist/{baseline-CuKFq4IF.js → baseline-0u4df12v.js} +1 -1
  8. package/dist/{baseline-s9F3fXTN.js → baseline-CuAS2lMk.js} +6 -5
  9. package/dist/cli/index.js +306 -18
  10. package/dist/{collect-BkAQ0viT.js → collect-Cu1q9NTB.js} +53 -11
  11. package/dist/command-C3D7JWn6.js +160 -0
  12. package/dist/{component-7kEBjv_y.js → component-C3GL1Mnu.js} +41 -6
  13. package/dist/component-DKd3EHOg.js +2 -0
  14. package/dist/coverage-B_Y6l-Ra.js +522 -0
  15. package/dist/{crawl-Oxt2Gaqo.js → crawl-BUWPgaGW.js} +83 -9
  16. package/dist/eleventy/index.d.ts +28 -0
  17. package/dist/eleventy/index.js +19 -0
  18. package/dist/fingerprint-BjYV_0F7.js +67 -0
  19. package/dist/{frameworks-BYa3tULg.js → frameworks-B4ClIJgE.js} +71 -0
  20. package/dist/{frameworks-DaDqrJOw.js → frameworks-etFg_O8K.js} +1 -1
  21. package/dist/{html-DKiI_3gs.js → html-C4vktg0A.js} +130 -5
  22. package/dist/{impact-YdoOtFqm.js → impact-DZt2oBCP.js} +1 -1
  23. package/dist/index.d.ts +119 -4
  24. package/dist/index.js +2 -2
  25. package/dist/{init-DiLiYvN1.js → init-CW7LfGT5.js} +10 -4
  26. package/dist/jsdom-22Bkt65v.js +3 -0
  27. package/dist/jsdom-B--cEH-G.js +89 -0
  28. package/dist/{json-Cnv9nd6U.js → json-DROX33kh.js} +9 -4
  29. package/dist/{json-B0Y7rNjt.js → json-D_Mnnft5.js} +1 -1
  30. package/dist/load-CFq2VQtT.js +2 -0
  31. package/dist/{load-UYXLqGV9.js → load-yAR4wzez.js} +133 -8
  32. package/dist/nuxt/index.d.ts +35 -0
  33. package/dist/nuxt/index.js +23 -0
  34. package/dist/{playwright-DYFsGUNd.js → playwright-BojtYVUa.js} +30 -6
  35. package/dist/{pool-BWkWZiJW.js → pool-BO25OIez.js} +52 -2
  36. package/dist/{project-DW08TseF.js → project-CzOnkLH6.js} +8 -2
  37. package/dist/project-MFrXcw1M.js +2 -0
  38. package/dist/remediation-CMBIrnpN.js +321 -0
  39. package/dist/{render-DI_aCnAZ.js → render-DbGOVmhx.js} +48 -10
  40. package/dist/{result-DLxd2Eip.js → result-BWcYXeRs.js} +114 -3
  41. package/dist/routes-CmdRUuOs.js +265 -0
  42. package/dist/{run-BW6CVuND.js → run-C2nKFcb-.js} +19 -2
  43. package/dist/{run-BMASMmwO.d.ts → run-CtcEUhbe.d.ts} +7 -0
  44. package/dist/{sarif-DB3WG7T9.js → sarif-B-UBcVu8.js} +16 -8
  45. package/dist/{schema-CMZ8ItGk.js → schema-DJSF4K05.js} +18 -1
  46. package/dist/statement/templates/es.en.md +125 -0
  47. package/dist/statement/templates/es.es.md +127 -0
  48. package/dist/statement/templates/fr.en.md +128 -0
  49. package/dist/statement/templates/fr.fr.md +131 -0
  50. package/dist/statement/templates/it.en.md +127 -0
  51. package/dist/statement/templates/it.it.md +130 -0
  52. package/dist/statement/templates/nl.en.md +125 -0
  53. package/dist/statement/templates/nl.nl.md +127 -0
  54. package/dist/{text-BFmNtMsV.js → text-CKKpzkYM.js} +1 -1
  55. package/dist/vite/index.d.ts +1 -1
  56. package/dist/vite/index.js +1 -1
  57. package/dist/webpack/index.d.ts +33 -0
  58. package/dist/webpack/index.js +20 -0
  59. package/package.json +35 -9
  60. package/dist/audit-CuG2hYyo.js +0 -2
  61. package/dist/command-Dxpa00Ha.js +0 -77
  62. package/dist/fingerprint-DRoneAjj.js +0 -20
  63. package/dist/jsdom-BjpF-2V-.js +0 -158
  64. package/dist/jsdom-X4KYfTp8.js +0 -3
  65. package/dist/manual-Vz-oX1I_.js +0 -239
  66. package/dist/routes-C2Cgf6Ko.js +0 -119
@@ -1,6 +1,15 @@
1
1
  import { pathToFileURL } from "node:url";
2
2
  import axe from "axe-core";
3
3
  //#region src/audit/result.ts
4
+ /**
5
+ * Per-page ceiling for an audit, in milliseconds.
6
+ *
7
+ * Lives here rather than in the jsdom runner because the worker pool needs it
8
+ * to set its own deadline, and the pool deliberately does not import jsdom —
9
+ * loading 630 ms of dependency to supervise threads that each load their own
10
+ * would be pure overhead. The same reason ENGINE_BLIND_RULES sits here.
11
+ */
12
+ const DEFAULT_PAGE_TIMEOUT_MS = 3e4;
4
13
  /** WCAG 2.2 AA and everything it builds on. Best-practice rules stay off. */
5
14
  const DEFAULT_TAGS = [
6
15
  "wcag2a",
@@ -10,6 +19,83 @@ const DEFAULT_TAGS = [
10
19
  "wcag22aa"
11
20
  ];
12
21
  /**
22
+ * Rules this engine structurally cannot decide.
23
+ *
24
+ * jsdom has no layout: every element reports a 0x0 box, computed style is
25
+ * limited to the inline cascade, and nothing is ever fetched. axe-core does not
26
+ * know that, so it happily returns `pass` for some of these — `target-size`
27
+ * passes on any page because a 0x0 target gets measured against nothing, and
28
+ * `color-contrast` passes on pages whose colours were never computed. Both were
29
+ * observed on real sites. Reporting either as a pass would be a false clean
30
+ * bill of health, so every rule listed here is force-reported as incomplete no
31
+ * matter which bucket axe-core put it in.
32
+ *
33
+ * The browser runner passes an empty map instead: with real layout these rules
34
+ * are exactly the ones it exists to answer.
35
+ *
36
+ * Rules tagged `experimental` are listed for completeness but filtered out at
37
+ * scope time: axe-core does not run them by default, so a browser run would not
38
+ * evaluate them either and telling the user to re-run with --browser would be
39
+ * misleading.
40
+ */
41
+ const ENGINE_BLIND_RULES = {
42
+ "color-contrast": {
43
+ detail: "needs rendered foreground and background colours",
44
+ applicabilityUnreliable: false
45
+ },
46
+ "color-contrast-enhanced": {
47
+ detail: "needs rendered foreground and background colours",
48
+ applicabilityUnreliable: false
49
+ },
50
+ "target-size": {
51
+ detail: "needs element geometry; every box is 0x0 without layout",
52
+ applicabilityUnreliable: false
53
+ },
54
+ "scrollable-region-focusable": {
55
+ detail: "needs computed overflow",
56
+ applicabilityUnreliable: true
57
+ },
58
+ "link-in-text-block": {
59
+ detail: "needs rendered colours and text decoration",
60
+ applicabilityUnreliable: true
61
+ },
62
+ "no-autoplay-audio": {
63
+ detail: "needs media duration, and media is never loaded",
64
+ applicabilityUnreliable: true
65
+ },
66
+ "avoid-inline-spacing": {
67
+ detail: "needs computed spacing after the full cascade",
68
+ applicabilityUnreliable: false
69
+ },
70
+ "p-as-heading": {
71
+ detail: "needs computed font size and weight",
72
+ applicabilityUnreliable: false
73
+ },
74
+ "css-orientation-lock": {
75
+ detail: "needs CSS media query evaluation",
76
+ applicabilityUnreliable: true
77
+ }
78
+ };
79
+ const blindScopeCache = /* @__PURE__ */ new Map();
80
+ /**
81
+ * Blind rules the requested tag filter would actually have run. Experimental
82
+ * rules are excluded: axe-core leaves them off by default, so a browser run
83
+ * would not have evaluated them either.
84
+ */
85
+ function blindRulesInScope(tags) {
86
+ const key = [...tags].sort().join(",");
87
+ const cached = blindScopeCache.get(key);
88
+ if (cached) return cached;
89
+ const inScope = /* @__PURE__ */ new Map();
90
+ for (const rule of axe.getRules([...tags])) {
91
+ if (rule.tags.includes("experimental")) continue;
92
+ const blind = ENGINE_BLIND_RULES[rule.ruleId];
93
+ if (blind) inScope.set(rule.ruleId, blind);
94
+ }
95
+ blindScopeCache.set(key, inScope);
96
+ return inScope;
97
+ }
98
+ /**
13
99
  * The URL a page is audited under, shared by every runner so their reports name
14
100
  * the same page. Without a base URL that is the file it came off disk as.
15
101
  */
@@ -45,17 +131,42 @@ function ruleOutcomes(audit) {
45
131
  ...audit.inapplicable
46
132
  ];
47
133
  }
48
- function runOptions(tags) {
134
+ function runOptions(tags, options = {}) {
49
135
  return {
50
136
  runOnly: {
51
137
  type: "tag",
52
138
  values: [...tags]
53
139
  },
54
140
  resultTypes: ["violations", "incomplete"],
55
- preload: false
141
+ preload: false,
142
+ ...options.skipBlindRules ? { rules: disabledBlindRules() } : {}
56
143
  };
57
144
  }
58
145
  /**
146
+ * The rules this engine cannot decide, switched off rather than run.
147
+ *
148
+ * Normally they are run and their verdict discarded: axe-core computes colour
149
+ * contrast against a stylesheet jsdom never fetched, and `shapeResults` throws
150
+ * the answer away as untrustworthy. The work is wasted either way, and it is
151
+ * not a small amount — measured at 14-19% of a page, since colour contrast is
152
+ * the most expensive rule axe-core has.
153
+ *
154
+ * What is lost is the element list. A rule that runs reports *which* elements
155
+ * it could not decide, and those are the elements a person has to check by
156
+ * hand; a rule that never runs cannot name them. That is the whole trade, and
157
+ * it is why this is behind a flag rather than the default.
158
+ *
159
+ * What does not change is the verdict. `shapeResults` already has a pass for
160
+ * rules axe-core skipped entirely — the preload-dependent ones arrive that way
161
+ * — and files them as unevaluated with the same reason. A criterion this run
162
+ * could not reach still reads as unreached.
163
+ */
164
+ function disabledBlindRules() {
165
+ const rules = {};
166
+ for (const ruleId of Object.keys(ENGINE_BLIND_RULES)) rules[ruleId] = { enabled: false };
167
+ return rules;
168
+ }
169
+ /**
59
170
  * Turn raw axe-core output into a PageAudit, shared by both engines so their
60
171
  * reports are directly comparable.
61
172
  *
@@ -202,4 +313,4 @@ function enClauses(tags) {
202
313
  return [...clauses].sort();
203
314
  }
204
315
  //#endregion
205
- export { ruleOutcomes as a, pageUrl as i, failedPage as n, runOptions as o, findingElements as r, shapeResults as s, DEFAULT_TAGS as t };
316
+ export { failedPage as a, ruleOutcomes as c, successCriteria as d, blindRulesInScope as i, runOptions as l, DEFAULT_TAGS as n, findingElements as o, ENGINE_BLIND_RULES as r, pageUrl as s, DEFAULT_PAGE_TIMEOUT_MS as t, shapeResults as u };
@@ -0,0 +1,265 @@
1
+ import { i as toPosix, n as isDirectory } from "./fs-BmPtmFke.js";
2
+ import { i as detectFramework } from "./frameworks-B4ClIJgE.js";
3
+ import { i as readPackageJson } from "./project-CzOnkLH6.js";
4
+ import path from "node:path";
5
+ import { glob } from "tinyglobby";
6
+ //#region src/audit/routes.ts
7
+ /**
8
+ * A path segment that serves more than one page, or none.
9
+ *
10
+ * Dynamic segments are refused rather than resolved: `[slug]` stands for every
11
+ * post there is, and picking one would name a file that did not produce the
12
+ * page in front of the reader. Private and slot folders contribute no route at
13
+ * all.
14
+ */
15
+ function classifyBracketSegment(segment) {
16
+ if (/^\(.*\)$/.test(segment)) return "skip";
17
+ if (/^[[(]|^_|^@/.test(segment)) return "refuse";
18
+ return "route";
19
+ }
20
+ /** Drop the extension, and fold an index file into the directory it sits in. */
21
+ function withoutExtension(file) {
22
+ const route = file.replace(/\.[^./]+$/, "").replace(/\/index$/, "");
23
+ return route === "index" ? "" : route;
24
+ }
25
+ /** The directory a route file sits in, for routers that put the route there. */
26
+ function containingDirectory(file) {
27
+ return file.includes("/") ? file.replace(/\/[^/]+$/, "") : "";
28
+ }
29
+ /** Keep the segments that are routes, refusing the file if any is dynamic. */
30
+ function joinSegments(route) {
31
+ const kept = [];
32
+ for (const segment of route.split("/").filter((part) => part !== "")) {
33
+ const kind = classifyBracketSegment(segment);
34
+ if (kind === "refuse") return void 0;
35
+ if (kind === "skip") continue;
36
+ kept.push(segment);
37
+ }
38
+ return kept.join("/");
39
+ }
40
+ /** `pages/kontakt.tsx` serves `/kontakt`; the filename is the route. */
41
+ function fileNamedRoute(file) {
42
+ return joinSegments(withoutExtension(file));
43
+ }
44
+ /** `app/kontakt/page.tsx` serves `/kontakt`; the directory is the route. */
45
+ function directoryNamedRoute(file) {
46
+ return joinSegments(containingDirectory(file));
47
+ }
48
+ /**
49
+ * A documentation tree whose directory layout is the URL layout.
50
+ *
51
+ * No dynamic segments to worry about — these are files an author wrote, one per
52
+ * page — so the only work is folding the index file into its directory.
53
+ */
54
+ function mirroredRoute(file) {
55
+ return withoutExtension(file);
56
+ }
57
+ /**
58
+ * Remix and React Router flat routes: `blog.post.tsx` serves `/blog/post`.
59
+ *
60
+ * The dot is the separator, `_index` is a directory's own page, and a trailing
61
+ * underscore on a segment escapes a parent layout without changing the URL. A
62
+ * `$` segment is dynamic and refused, as `[slug]` is elsewhere.
63
+ */
64
+ function remixRoute(file) {
65
+ const withoutRouteFile = file.replace(/\/route\.[^./]+$/, "");
66
+ const flat = withoutRouteFile === file ? withoutExtension(file) : toPosix(withoutRouteFile);
67
+ const leaf = flat.includes("/") ? flat.split("/").pop() : flat;
68
+ if (leaf === "") return void 0;
69
+ const kept = [];
70
+ for (const raw of leaf.split(".")) {
71
+ const segment = raw.replace(/_$/, "");
72
+ if (segment === "") continue;
73
+ if (segment === "_index") continue;
74
+ if (segment.startsWith("$")) return void 0;
75
+ if (segment.includes("[")) return void 0;
76
+ kept.push(segment);
77
+ }
78
+ return kept.join("/");
79
+ }
80
+ /**
81
+ * Hugo content: `content/posts/hello.md` serves `/posts/hello/`.
82
+ *
83
+ * `_index.md` is a section's own page rather than a page called `_index`, which
84
+ * is the one place Hugo's leading underscore does not mean "private".
85
+ */
86
+ function hugoRoute(file) {
87
+ const withoutIndex = file.replace(/(^|\/)_index\.[^./]+$/, "");
88
+ if (withoutIndex !== file) return toPosix(withoutIndex);
89
+ return withoutExtension(file);
90
+ }
91
+ /**
92
+ * Conventions, most specific first.
93
+ *
94
+ * Order still matters where the registry knows nothing about a project, which
95
+ * is the case for a plain directory of HTML and for anything the registry has
96
+ * not heard of.
97
+ */
98
+ const CONVENTIONS = [
99
+ {
100
+ framework: "next-app",
101
+ ids: ["next"],
102
+ dir: "app",
103
+ pattern: "**/page.{tsx,ts,jsx,js,mdx}",
104
+ toRoute: directoryNamedRoute
105
+ },
106
+ {
107
+ framework: "next-app",
108
+ ids: ["next"],
109
+ dir: "src/app",
110
+ pattern: "**/page.{tsx,ts,jsx,js,mdx}",
111
+ toRoute: directoryNamedRoute
112
+ },
113
+ {
114
+ framework: "remix",
115
+ ids: ["remix"],
116
+ dir: "app/routes",
117
+ pattern: "**/*.{tsx,ts,jsx,js,mdx}",
118
+ toRoute: remixRoute
119
+ },
120
+ {
121
+ framework: "starlight",
122
+ ids: ["astro"],
123
+ dir: "src/content/docs",
124
+ pattern: "**/*.{md,mdx,mdoc}",
125
+ toRoute: mirroredRoute
126
+ },
127
+ {
128
+ framework: "astro",
129
+ ids: ["astro"],
130
+ dir: "src/pages",
131
+ pattern: "**/*.{astro,md,mdx,html}",
132
+ toRoute: fileNamedRoute
133
+ },
134
+ {
135
+ framework: "gatsby",
136
+ ids: ["gatsby"],
137
+ dir: "src/pages",
138
+ pattern: "**/*.{tsx,ts,jsx,js,md,mdx}",
139
+ toRoute: fileNamedRoute
140
+ },
141
+ {
142
+ framework: "next-pages",
143
+ ids: ["next"],
144
+ dir: "pages",
145
+ pattern: "**/*.{tsx,ts,jsx,js,mdx}",
146
+ toRoute: fileNamedRoute
147
+ },
148
+ {
149
+ framework: "next-pages",
150
+ ids: ["next"],
151
+ dir: "src/pages",
152
+ pattern: "**/*.{tsx,ts,jsx,js,mdx}",
153
+ toRoute: fileNamedRoute
154
+ },
155
+ {
156
+ framework: "nuxt",
157
+ ids: ["nuxt"],
158
+ dir: "pages",
159
+ pattern: "**/*.vue",
160
+ toRoute: fileNamedRoute
161
+ },
162
+ {
163
+ framework: "sveltekit",
164
+ ids: ["sveltekit"],
165
+ dir: "src/routes",
166
+ pattern: "**/+page.{svelte,ts,js}",
167
+ toRoute: directoryNamedRoute
168
+ },
169
+ {
170
+ framework: "docusaurus",
171
+ ids: ["docusaurus"],
172
+ dir: "docs",
173
+ pattern: "**/*.{md,mdx}",
174
+ toRoute: (file) => {
175
+ const route = mirroredRoute(file);
176
+ return route === void 0 ? void 0 : route === "" ? "docs" : `docs/${route}`;
177
+ }
178
+ },
179
+ {
180
+ framework: "vitepress",
181
+ ids: ["vitepress"],
182
+ dir: "docs",
183
+ pattern: "**/*.md",
184
+ toRoute: mirroredRoute
185
+ },
186
+ {
187
+ framework: "hugo",
188
+ ids: ["hugo"],
189
+ dir: "content",
190
+ pattern: "**/*.{md,html}",
191
+ toRoute: hugoRoute
192
+ }
193
+ ];
194
+ /** Every page path a build could have emitted for one route. */
195
+ function emittedPathsFor(route) {
196
+ if (route === "") return [
197
+ "/",
198
+ "index.html",
199
+ "index.htm"
200
+ ];
201
+ return [
202
+ route,
203
+ `${route}/`,
204
+ `${route}.html`,
205
+ `${route}/index.html`
206
+ ];
207
+ }
208
+ /**
209
+ * Build a page-to-source map by reading the project's route files.
210
+ *
211
+ * Returns undefined when the project uses no convention this understands, which
212
+ * is not a failure — most builds are not framework projects.
213
+ */
214
+ async function buildRouteMap(cwd, pkg) {
215
+ for (const convention of await orderedConventions(cwd, pkg)) {
216
+ const map = await mapConvention(cwd, convention);
217
+ if (map !== void 0) return map;
218
+ }
219
+ }
220
+ /**
221
+ * Conventions to try, the detected framework's first.
222
+ *
223
+ * Without this, `src/pages` is claimed by whichever convention is declared
224
+ * earliest, so a Gatsby project was reported as `next-pages`. The mapping
225
+ * happened to be right and the name was wrong, which is the kind of detail a
226
+ * reader notices and stops trusting the rest of the report over.
227
+ */
228
+ async function orderedConventions(cwd, pkg) {
229
+ const detected = await detectFramework(cwd, pkg ?? await readPackageJson(cwd));
230
+ if (detected === void 0) return [...CONVENTIONS];
231
+ const id = detected.framework.id;
232
+ const mine = CONVENTIONS.filter((convention) => convention.ids.includes(id));
233
+ const rest = CONVENTIONS.filter((convention) => !convention.ids.includes(id));
234
+ return [...mine, ...rest];
235
+ }
236
+ /** Read one convention's directory, or undefined when it has nothing to say. */
237
+ async function mapConvention(cwd, convention) {
238
+ const directory = path.join(cwd, convention.dir);
239
+ if (!await isDirectory(directory)) return void 0;
240
+ const files = await glob([convention.pattern], {
241
+ cwd: directory,
242
+ ignore: ["**/node_modules/**"],
243
+ onlyFiles: true
244
+ });
245
+ if (files.length === 0) return void 0;
246
+ const sources = /* @__PURE__ */ new Map();
247
+ for (const file of files) {
248
+ const route = convention.toRoute(toPosix(file));
249
+ if (route === void 0) continue;
250
+ const source = `${convention.dir}/${toPosix(file)}`;
251
+ for (const emitted of emittedPathsFor(route)) if (!sources.has(emitted)) sources.set(emitted, source);
252
+ }
253
+ if (sources.size === 0) return void 0;
254
+ return {
255
+ framework: convention.framework,
256
+ sources
257
+ };
258
+ }
259
+ /** The source file for an audited page, if the map knows one. */
260
+ function sourceFor(map, pagePath) {
261
+ if (map === void 0) return void 0;
262
+ return map.sources.get(pagePath) ?? map.sources.get(pagePath.replace(/^\//, ""));
263
+ }
264
+ //#endregion
265
+ export { buildRouteMap, sourceFor };
@@ -1,5 +1,22 @@
1
1
  //#region src/integration/run.ts
2
2
  /**
3
+ * Where an integration writes when its host offers no logger of its own.
4
+ *
5
+ * stderr rather than stdout, so a line about accessibility never lands in the
6
+ * middle of a report being piped to a file, and prefixed either way: a message
7
+ * in somebody's build log has to say what produced it.
8
+ */
9
+ function stderrLogger() {
10
+ const write = (message) => {
11
+ process.stderr.write(`eaa-kit: ${message}\n`);
12
+ };
13
+ return {
14
+ info: write,
15
+ warn: write,
16
+ error: write
17
+ };
18
+ }
19
+ /**
3
20
  * Thrown to fail the build. Its own class so a consumer can tell an audit
4
21
  * failure from the build tool falling over.
5
22
  */
@@ -18,7 +35,7 @@ async function auditBuild(directory, options, logger) {
18
35
  logger.info("skipped (enabled: false)");
19
36
  return;
20
37
  }
21
- const { runAuditCommand } = await import("./audit-CuG2hYyo.js");
38
+ const { runAuditCommand } = await import("./audit-DyHPmGeD.js");
22
39
  const { exitCode } = await runAuditCommand(directory, auditOptions);
23
40
  if (exitCode === 0) {
24
41
  logger.info("no violations at or above the threshold");
@@ -33,4 +50,4 @@ async function auditBuild(directory, options, logger) {
33
50
  throw new BuildAuditError(`eaa-kit: ${message}`);
34
51
  }
35
52
  //#endregion
36
- export { auditBuild as n, BuildAuditError as t };
53
+ export { auditBuild as n, stderrLogger as r, BuildAuditError as t };
@@ -20,6 +20,13 @@ interface IntegrationOptions {
20
20
  baseUrl?: string;
21
21
  /** Audit in real Chromium. Needs the playwright peer. */
22
22
  browser?: boolean;
23
+ /**
24
+ * Skip the rules the browserless engine cannot decide rather than running
25
+ * them and discarding the answer. The verdict does not move — those rules are
26
+ * still reported as not evaluated — so what a build gives up is the list of
27
+ * elements a person would check by hand. No effect under `browser`.
28
+ */
29
+ fast?: boolean;
23
30
  concurrency?: number;
24
31
  baseline?: string;
25
32
  format?: OutputFormat;
@@ -1,8 +1,8 @@
1
- import { o as isImpactLevel } from "./impact-YdoOtFqm.js";
2
- import { a as standardsReference } from "./text-BFmNtMsV.js";
1
+ import { o as isImpactLevel } from "./impact-DZt2oBCP.js";
2
+ import { o as standardsReference } from "./text-CKKpzkYM.js";
3
3
  import { t as TOOL_VERSION } from "./version-B3v4rNoG.js";
4
- import { t as elementFingerprint } from "./fingerprint-DRoneAjj.js";
5
- import { a as ruleOutcomes, r as findingElements } from "./result-DLxd2Eip.js";
4
+ import { t as elementFingerprint } from "./fingerprint-BjYV_0F7.js";
5
+ import { c as ruleOutcomes, o as findingElements } from "./result-BWcYXeRs.js";
6
6
  import path from "node:path";
7
7
  //#region src/audit/report/sarif.ts
8
8
  const SARIF_VERSION = "2.1.0";
@@ -75,7 +75,7 @@ function buildSarifReport(audits, options) {
75
75
  toolExecutionNotifications: notifications
76
76
  }],
77
77
  results,
78
- properties: summaryProperties(audits)
78
+ properties: summaryProperties(audits, options.completeness)
79
79
  }]
80
80
  };
81
81
  }
@@ -111,7 +111,7 @@ function toResults(finding, ruleIndex, uri, suppressions) {
111
111
  * combines the fingerprint with the location itself.
112
112
  */
113
113
  function fingerprint(ruleId, selector, html) {
114
- return { "eaaKit/v1": elementFingerprint(ruleId, selector, html) };
114
+ return { "eaaKit/v2": elementFingerprint(ruleId, selector, html) };
115
115
  }
116
116
  /** Every rule the run knows about, so the catalogue is complete in GitHub. */
117
117
  function buildRules(audits) {
@@ -140,7 +140,7 @@ function toSarifRule(outcome) {
140
140
  * Coverage that has no place in `results` but should not vanish: a SARIF log
141
141
  * with no results must not be mistaken for "everything was checked".
142
142
  */
143
- function summaryProperties(audits) {
143
+ function summaryProperties(audits, completeness) {
144
144
  let needsReview = 0;
145
145
  let notEvaluated = 0;
146
146
  const notEvaluatedRules = /* @__PURE__ */ new Set();
@@ -153,7 +153,15 @@ function summaryProperties(audits) {
153
153
  pages: audits.length,
154
154
  needsReview,
155
155
  notEvaluated,
156
- notEvaluatedRules: [...notEvaluatedRules].sort()
156
+ notEvaluatedRules: [...notEvaluatedRules].sort(),
157
+ ...completeness ? {
158
+ complete: completeness.complete,
159
+ discovery: completeness.discovery,
160
+ pagesAudited: completeness.audited,
161
+ pagesErrored: completeness.errored,
162
+ pagesUnreachable: completeness.unreachable.length,
163
+ truncated: completeness.truncated
164
+ } : {}
157
165
  };
158
166
  }
159
167
  /**
@@ -64,9 +64,26 @@ function string(options = {}) {
64
64
  return value;
65
65
  } };
66
66
  }
67
+ function boolean() {
68
+ return { read: (value, path, issues) => typeof value === "boolean" ? value : fail(issues, path, "expected true or false") };
69
+ }
67
70
  function number() {
68
71
  return { read: (value, path, issues) => typeof value === "number" && Number.isFinite(value) ? value : fail(issues, path, "expected a number") };
69
72
  }
73
+ /**
74
+ * A whole number, with an optional floor.
75
+ *
76
+ * `number` would accept 2.5 pages and a concurrency of 0, both of which the
77
+ * CLI's own parsers refuse. A config file is the same instruction typed
78
+ * somewhere else, so it is held to the same rule.
79
+ */
80
+ function integer(options = {}) {
81
+ return { read: (value, path, issues) => {
82
+ if (typeof value !== "number" || !Number.isInteger(value)) return fail(issues, path, "expected a whole number");
83
+ if (options.min !== void 0 && value < options.min) return fail(issues, path, `must be ${options.min} or more`);
84
+ return value;
85
+ } };
86
+ }
70
87
  /** One of a fixed set. The message lists them, since that is the useful part. */
71
88
  function enumeration(values) {
72
89
  return { read: (value, path, issues) => typeof value === "string" && values.includes(value) ? value : fail(issues, path, `expected one of ${values.join(", ")}`) };
@@ -189,4 +206,4 @@ function email() {
189
206
  } };
190
207
  }
191
208
  //#endregion
192
- export { withDefault as _, isoDateTime as a, object as c, record as d, safeParse as f, url as g, union as h, isoDate as i, optional as l, transform as m, email as n, nullable as o, string as p, enumeration as r, number as s, array as t, pipe as u };
209
+ export { union as _, integer as a, nullable as c, optional as d, pipe as f, transform as g, string as h, enumeration as i, number as l, safeParse as m, boolean as n, isoDate as o, record as p, email as r, isoDateTime as s, array as t, object as u, url as v, withDefault as y };
@@ -0,0 +1,125 @@
1
+ # Accessibility Statement
2
+
3
+ {{ provider.legalName }} is committed to making the website {{ site.name }} accessible in
4
+ accordance with Ley 11/2023 of 8 May 2023, which transposes Directive (EU) 2019/882 (the
5
+ European Accessibility Act) into Spanish law. Its obligations for products and services
6
+ apply from 28 June 2025.
7
+
8
+ This accessibility statement applies to {{ site.url }}.
9
+
10
+ ## Compliance status
11
+
12
+ {{#if compliance.isCompliant}}
13
+ This website is fully compliant with {{ compliance.standard }}.
14
+ {{/if}}
15
+ {{#if compliance.isPartiallyCompliant}}
16
+ This website is partially compliant with {{ compliance.standard }}. The content listed in
17
+ the following section is not accessible, for the reasons given.
18
+ {{/if}}
19
+ {{#if compliance.isNonCompliant}}
20
+ This website is not compliant with {{ compliance.standard }}. The content listed in the
21
+ following section is not accessible, for the reasons given.
22
+ {{/if}}
23
+
24
+ ## Non-accessible content
25
+
26
+ {{#if hasKnownIssues}}
27
+ {{#each compliance.knownIssues}}
28
+ - {{ description }}
29
+ {{#if standards}}
30
+ Requirement affected: {{ standards }}
31
+ {{/if}}
32
+ {{#if pageList}}
33
+ Pages affected: {{ pageList }}{{#if hasMorePages}} and {{ morePages }} more{{/if}}
34
+ {{/if}}
35
+ {{#if isDisproportionateBurden}}
36
+ Reason: disproportionate burden.
37
+ {{/if}}
38
+ {{#if isOutOfScope}}
39
+ Reason: the content falls outside the scope of Ley 11/2023.
40
+ {{/if}}
41
+ {{#if isFixPlanned}}
42
+ Reason: the barrier is known and is being addressed.
43
+ {{/if}}
44
+ {{#if remedyByFormatted}}
45
+ Expected to be resolved by: {{ remedyByFormatted }}
46
+ {{/if}}
47
+ {{#if isFromAudit}}
48
+ Detected by automated testing (axe-core, rule {{ ruleId }}); describe it in your own words.
49
+ {{/if}}
50
+ {{/each}}
51
+ {{/if}}
52
+ {{#if hasNoKnownIssues}}
53
+ No non-accessible content was known at the time of assessment.
54
+ {{/if}}
55
+
56
+ ## Preparation of this statement
57
+
58
+ This statement was prepared on {{ compliance.assessedOnFormatted }}.
59
+
60
+ {{#if compliance.isSelfAssessment}}
61
+ It is based on a self-assessment carried out by {{ provider.legalName }}.
62
+ {{/if}}
63
+ {{#if compliance.isExternalAudit}}
64
+ It is based on an assessment carried out by a third party.
65
+ {{/if}}
66
+
67
+ {{#if audit.isSinglePage}}
68
+ The automated test run of {{ audit.checkedOnFormatted }} covered one page of this website.
69
+ {{/if}}
70
+ {{#if audit.isMultiPage}}
71
+ The automated test run of {{ audit.checkedOnFormatted }} covered {{ audit.pages }}
72
+ pages of this website.
73
+ {{/if}}
74
+ {{#if audit.needsReviewIsSingle}}
75
+ One further rule check requires a manual decision.
76
+ {{/if}}
77
+ {{#if audit.needsReviewIsPlural}}
78
+ {{ audit.needsReview }} further rule checks require a manual decision.
79
+ {{/if}}
80
+ {{#if audit.notEvaluatedIsSingle}}
81
+ One rule check could not be decided by the tool that was used; it is not reported as met.
82
+ {{/if}}
83
+ {{#if audit.notEvaluatedIsPlural}}
84
+ {{ audit.notEvaluated }} rule checks could not be decided by the tool that was used; they
85
+ are not reported as met.
86
+ {{/if}}
87
+ {{#if hasAudit}}
88
+
89
+ {{/if}}
90
+ The assessment relies in part on automated testing. Automated tools detect only a subset
91
+ of possible barriers; they are not a substitute for manual testing or for testing with
92
+ assistive technologies.
93
+
94
+ ## Feedback and contact
95
+
96
+ Found a barrier, or need information in an accessible format? Please get in touch:
97
+
98
+ - Email: {{ provider.email }}
99
+ {{#if provider.feedbackUrl}}
100
+ - Contact form: {{ provider.feedbackUrl }}
101
+ {{/if}}
102
+ {{#if provider.phone}}
103
+ - Phone: {{ provider.phone }}
104
+ {{/if}}
105
+ {{#if provider.address}}
106
+ - Address: {{ provider.address }}
107
+ {{/if}}
108
+
109
+ We aim to respond to your feedback promptly.
110
+
111
+ ## Enforcement procedure
112
+
113
+ If you are not satisfied with our response, you can complain to the competent market
114
+ surveillance authority. Ley 11/2023 divides that surveillance between the national
115
+ administration and the autonomous communities, so for a digital service the usual route is
116
+ the consumer authority of your autonomous community.
117
+
118
+ Public-sector websites and mobile applications are additionally governed by Real Decreto
119
+ 1112/2018, which requires an accessibility statement with content of its own. This document
120
+ is not that statement.
121
+
122
+ ---
123
+
124
+ This statement was generated with eaa-kit and is not legal advice. Review it before
125
+ publishing, and have it checked by a lawyer if in doubt.