ultra11y 2.22.0 → 2.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -9
- package/package.json +6 -2
- package/scripts/build-pack-rgaa.mjs +95 -1
- package/scripts/build-standards.mjs +5 -1
- package/scripts/ultra11y.mjs +2920 -320
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +2920 -320
- package/skills/ultra11y/SKILL.md +38 -2
- package/skills/ultra11y/references/ci.md +165 -0
- package/skills/ultra11y/references/criteria.md +5 -5
- package/skills/ultra11y/references/devtools.md +67 -0
- package/skills/ultra11y/references/e2e.md +112 -0
- package/skills/ultra11y/references/judgment.md +25 -0
- package/skills/ultra11y/references/pages.md +205 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +2920 -320
- package/src/adjudicate.ts +270 -45
- package/src/annotate.ts +158 -0
- package/src/audit.ts +8 -1
- package/src/capture.ts +4 -0
- package/src/cli.ts +485 -14
- package/src/criteria.ts +58 -0
- package/src/data/standards/rgaa.json +155 -30
- package/src/data/wcag.json +5 -5
- package/src/dev.ts +427 -0
- package/src/e2e.ts +303 -0
- package/src/gh.ts +72 -0
- package/src/init.ts +39 -7
- package/src/messages.ts +87 -0
- package/src/orchestrate-templates.ts +12 -4
- package/src/pages.ts +283 -0
- package/src/parse/html.ts +12 -0
- package/src/pixel.ts +217 -0
- package/src/prd.ts +37 -0
- package/src/report.ts +28 -2
- package/src/rules/registry.ts +2 -0
- package/src/rules/rendered.ts +379 -0
- package/src/rules/rule.ts +3 -0
- package/src/sarif.ts +196 -0
- package/src/scan-local.ts +4 -0
- package/src/snapshot.ts +442 -0
- package/src/standards/derive.ts +32 -1
- package/src/types.ts +161 -1
- package/src/util.ts +18 -0
package/scripts/ultra11y.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { realpathSync as realpathSync4, writeFileSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { realpathSync as realpathSync4, writeFileSync as writeFileSync14, mkdirSync as mkdirSync11, existsSync as existsSync22, readFileSync as readFileSync19, appendFileSync } from "fs";
|
|
5
|
+
import { join as join38, relative as relative3, sep as sep6, dirname as dirname11 } from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "2.
|
|
9
|
+
var VERSION = "2.24.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -385,7 +385,7 @@ var wcag_default = {
|
|
|
385
385
|
level: "AA",
|
|
386
386
|
addedIn: "2.1",
|
|
387
387
|
automatability: "needs-rendering",
|
|
388
|
-
ruleIds: [],
|
|
388
|
+
ruleIds: ["rendered-orientation-lock"],
|
|
389
389
|
understanding: "https://www.w3.org/WAI/WCAG22/Understanding/orientation.html"
|
|
390
390
|
},
|
|
391
391
|
{
|
|
@@ -410,7 +410,7 @@ var wcag_default = {
|
|
|
410
410
|
level: "A",
|
|
411
411
|
addedIn: "2.0",
|
|
412
412
|
automatability: "needs-rendering",
|
|
413
|
-
ruleIds: [],
|
|
413
|
+
ruleIds: ["rendered-link-colour-only"],
|
|
414
414
|
understanding: "https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html",
|
|
415
415
|
techniques: ["C15", "F14", "F26", "F73", "F78", "G111", "G117", "G138", "G14", "G140", "G149", "G165", "G182", "G183", "G195", "G205", "G96", "SCR31"]
|
|
416
416
|
},
|
|
@@ -436,7 +436,7 @@ var wcag_default = {
|
|
|
436
436
|
level: "AA",
|
|
437
437
|
addedIn: "2.0",
|
|
438
438
|
automatability: "needs-rendering",
|
|
439
|
-
ruleIds: ["contrast-literal"],
|
|
439
|
+
ruleIds: ["contrast-literal", "rendered-contrast", "rendered-contrast-pixel"],
|
|
440
440
|
understanding: "https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html",
|
|
441
441
|
techniques: ["C29", "F24", "G136", "G145", "G148", "G174", "G18"]
|
|
442
442
|
},
|
|
@@ -488,7 +488,7 @@ var wcag_default = {
|
|
|
488
488
|
level: "AA",
|
|
489
489
|
addedIn: "2.1",
|
|
490
490
|
automatability: "needs-rendering",
|
|
491
|
-
ruleIds: [],
|
|
491
|
+
ruleIds: ["rendered-nontext-contrast"],
|
|
492
492
|
understanding: "https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html",
|
|
493
493
|
techniques: ["F78", "G145", "G174", "G18", "G183", "G195", "G207"]
|
|
494
494
|
},
|
|
@@ -745,7 +745,7 @@ var wcag_default = {
|
|
|
745
745
|
level: "AA",
|
|
746
746
|
addedIn: "2.0",
|
|
747
747
|
automatability: "needs-rendering",
|
|
748
|
-
ruleIds: [],
|
|
748
|
+
ruleIds: ["rendered-focus-not-visible"],
|
|
749
749
|
understanding: "https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html",
|
|
750
750
|
techniques: ["C15", "F42", "F54", "F55", "F73", "F78", "G149", "G165", "G183", "G195", "G202", "G90", "SCR2", "SCR20", "SCR29", "SCR31", "SCR35"]
|
|
751
751
|
},
|
|
@@ -1770,6 +1770,9 @@ function scsByGuideline() {
|
|
|
1770
1770
|
function techniques(id) {
|
|
1771
1771
|
return byId.get(id)?.techniques ?? [];
|
|
1772
1772
|
}
|
|
1773
|
+
function understanding(id) {
|
|
1774
|
+
return byId.get(id)?.understanding;
|
|
1775
|
+
}
|
|
1773
1776
|
function meta() {
|
|
1774
1777
|
return {
|
|
1775
1778
|
wcagVersion: data.wcagVersion,
|
|
@@ -1779,6 +1782,15 @@ function meta() {
|
|
|
1779
1782
|
criteriaSource: data.criteriaSource
|
|
1780
1783
|
};
|
|
1781
1784
|
}
|
|
1785
|
+
function compareSC(a, b) {
|
|
1786
|
+
const pa = a.split(".").map(Number);
|
|
1787
|
+
const pb = b.split(".").map(Number);
|
|
1788
|
+
for (let i2 = 0; i2 < Math.max(pa.length, pb.length); i2++) {
|
|
1789
|
+
const d = (pa[i2] ?? 0) - (pb[i2] ?? 0);
|
|
1790
|
+
if (d) return d;
|
|
1791
|
+
}
|
|
1792
|
+
return 0;
|
|
1793
|
+
}
|
|
1782
1794
|
function techniquesFor(id) {
|
|
1783
1795
|
return getSC(id)?.techniques ?? [];
|
|
1784
1796
|
}
|
|
@@ -10708,12 +10720,12 @@ var Tokenizer2 = class extends CommentsParser {
|
|
|
10708
10720
|
continue;
|
|
10709
10721
|
}
|
|
10710
10722
|
++this.state.pos;
|
|
10711
|
-
const
|
|
10712
|
-
if (
|
|
10713
|
-
if (!identifierCheck(
|
|
10723
|
+
const esc3 = this.readCodePoint(true);
|
|
10724
|
+
if (esc3 !== null) {
|
|
10725
|
+
if (!identifierCheck(esc3)) {
|
|
10714
10726
|
this.raise(Errors.EscapedCharNotAnIdentifier, escStart);
|
|
10715
10727
|
}
|
|
10716
|
-
word += String.fromCodePoint(
|
|
10728
|
+
word += String.fromCodePoint(esc3);
|
|
10717
10729
|
}
|
|
10718
10730
|
chunkStart = this.state.pos;
|
|
10719
10731
|
} else {
|
|
@@ -18794,6 +18806,15 @@ function readText(path) {
|
|
|
18794
18806
|
function ext(path) {
|
|
18795
18807
|
return extname(path).toLowerCase();
|
|
18796
18808
|
}
|
|
18809
|
+
function isUrlPath(file) {
|
|
18810
|
+
return /^https?:\/\//i.test(file);
|
|
18811
|
+
}
|
|
18812
|
+
function repoRelative(file, baseDir) {
|
|
18813
|
+
const posix3 = file.split("\\").join("/").replace(/^\.\//, "");
|
|
18814
|
+
const base = baseDir.split("\\").join("/").replace(/\/+$/, "");
|
|
18815
|
+
if (base && posix3.startsWith(`${base}/`)) return posix3.slice(base.length + 1);
|
|
18816
|
+
return posix3;
|
|
18817
|
+
}
|
|
18797
18818
|
async function readStdin() {
|
|
18798
18819
|
const chunks = [];
|
|
18799
18820
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -31494,13 +31515,13 @@ function computeDelta(graph, symbols, diff, depth = DEFAULT_DELTA_DEPTH) {
|
|
|
31494
31515
|
const shown = exportedNames.slice(0, 3).join(", ") + (exportedNames.length > 3 ? ", \u2026" : "");
|
|
31495
31516
|
reasons.push(exportedNames.length === 1 ? `exported symbol ${shown} changed` : `exported symbols ${shown} changed`);
|
|
31496
31517
|
}
|
|
31497
|
-
const
|
|
31498
|
-
if (
|
|
31518
|
+
const pct2 = percentile(metricValues, metricOf(m));
|
|
31519
|
+
if (pct2 >= 0.9) {
|
|
31499
31520
|
score += RISK_WEIGHTS.hubHigh;
|
|
31500
|
-
reasons.push(`${metricName} p${Math.round(
|
|
31501
|
-
} else if (
|
|
31521
|
+
reasons.push(`${metricName} p${Math.round(pct2 * 100)} hub`);
|
|
31522
|
+
} else if (pct2 >= 0.75) {
|
|
31502
31523
|
score += RISK_WEIGHTS.hubMed;
|
|
31503
|
-
reasons.push(`${metricName} p${Math.round(
|
|
31524
|
+
reasons.push(`${metricName} p${Math.round(pct2 * 100)} hub`);
|
|
31504
31525
|
}
|
|
31505
31526
|
const depthByRel = /* @__PURE__ */ new Map();
|
|
31506
31527
|
const impModules = /* @__PURE__ */ new Set();
|
|
@@ -32456,6 +32477,8 @@ function formatCaptureComment(prov) {
|
|
|
32456
32477
|
if (prov.component) attrs.push(`component="${escapeCommentValue(prov.component)}"`);
|
|
32457
32478
|
if (prov.test) attrs.push(`test="${escapeCommentValue(prov.test)}"`);
|
|
32458
32479
|
if (prov.name) attrs.push(`name="${escapeCommentValue(prov.name)}"`);
|
|
32480
|
+
if (prov.page) attrs.push(`page="${escapeCommentValue(prov.page)}"`);
|
|
32481
|
+
if (prov.url) attrs.push(`url="${escapeCommentValue(prov.url)}"`);
|
|
32459
32482
|
return `<!-- ultra11y:capture ${attrs.join(" ")} -->`;
|
|
32460
32483
|
}
|
|
32461
32484
|
var SCAN_LIMIT = 4096;
|
|
@@ -32478,7 +32501,9 @@ function parseCaptureProvenance(source) {
|
|
|
32478
32501
|
...kv.has("source") ? { sourceFile: kv.get("source") } : {},
|
|
32479
32502
|
...kv.has("component") ? { component: kv.get("component") } : {},
|
|
32480
32503
|
...kv.has("test") ? { test: kv.get("test") } : {},
|
|
32481
|
-
...kv.has("name") ? { name: kv.get("name") } : {}
|
|
32504
|
+
...kv.has("name") ? { name: kv.get("name") } : {},
|
|
32505
|
+
...kv.has("page") ? { page: kv.get("page") } : {},
|
|
32506
|
+
...kv.has("url") ? { url: kv.get("url") } : {}
|
|
32482
32507
|
};
|
|
32483
32508
|
}
|
|
32484
32509
|
function pathMatch(a, b) {
|
|
@@ -32579,6 +32604,271 @@ function parseSourceWithAst(source, file, opts = {}) {
|
|
|
32579
32604
|
return { doc, ast: null };
|
|
32580
32605
|
}
|
|
32581
32606
|
|
|
32607
|
+
// src/snapshot.ts
|
|
32608
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync10, readdirSync as readdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
32609
|
+
import { dirname as dirname5, join as join21 } from "path";
|
|
32610
|
+
var SNAPSHOT_VERSION = 1;
|
|
32611
|
+
var PAGES_DIR = ".ultra11y/pages";
|
|
32612
|
+
var ID_RE = /^[a-z0-9][a-z0-9-]*$/i;
|
|
32613
|
+
function validateSnapshotMeta(raw) {
|
|
32614
|
+
const issues = [];
|
|
32615
|
+
const err2 = (path, message) => {
|
|
32616
|
+
issues.push({ path, message });
|
|
32617
|
+
};
|
|
32618
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
32619
|
+
err2("meta", "meta must be an object { v, id, name, url, \u2026 }");
|
|
32620
|
+
return { ok: false, issues };
|
|
32621
|
+
}
|
|
32622
|
+
const m = raw;
|
|
32623
|
+
const v = typeof m.v === "number" ? m.v : Number.NaN;
|
|
32624
|
+
if (!Number.isFinite(v) || v < 1) err2("meta.v", "v must be a positive integer snapshot-format version");
|
|
32625
|
+
else if (v > SNAPSHOT_VERSION) err2("meta.v", `snapshot format v${v} is newer than this engine understands (v${SNAPSHOT_VERSION}) \u2014 upgrade ultra11y`);
|
|
32626
|
+
if (typeof m.id !== "string" || !m.id.trim()) err2("meta.id", "id must be a non-empty string");
|
|
32627
|
+
else if (!ID_RE.test(m.id)) err2("meta.id", `id "${m.id}" must match ${ID_RE} (it becomes a directory name)`);
|
|
32628
|
+
if (typeof m.name !== "string" || !m.name.trim()) err2("meta.name", "name must be a non-empty string");
|
|
32629
|
+
if (typeof m.url !== "string" || !m.url.trim()) err2("meta.url", "url must be a non-empty string");
|
|
32630
|
+
if (m.auth !== void 0 && typeof m.auth !== "boolean") err2("meta.auth", "auth must be a boolean");
|
|
32631
|
+
if (m.route !== void 0 && typeof m.route !== "string") err2("meta.route", "route must be a string");
|
|
32632
|
+
if (m.notes !== void 0 && typeof m.notes !== "string") err2("meta.notes", "notes must be a string");
|
|
32633
|
+
if (m.sources !== void 0 && (!Array.isArray(m.sources) || m.sources.some((s) => typeof s !== "string")))
|
|
32634
|
+
err2("meta.sources", "sources must be an array of strings");
|
|
32635
|
+
if (issues.length) return { ok: false, issues };
|
|
32636
|
+
return {
|
|
32637
|
+
ok: true,
|
|
32638
|
+
issues,
|
|
32639
|
+
meta: {
|
|
32640
|
+
v,
|
|
32641
|
+
id: m.id,
|
|
32642
|
+
name: m.name,
|
|
32643
|
+
url: m.url,
|
|
32644
|
+
...typeof m.route === "string" ? { route: m.route } : {},
|
|
32645
|
+
...typeof m.auth === "boolean" ? { auth: m.auth } : {},
|
|
32646
|
+
...m.viewport && typeof m.viewport === "object" ? { viewport: m.viewport } : {},
|
|
32647
|
+
...typeof m.capturedAt === "string" ? { capturedAt: m.capturedAt } : {},
|
|
32648
|
+
...typeof m.runner === "string" ? { runner: m.runner } : {},
|
|
32649
|
+
...Array.isArray(m.sources) ? { sources: m.sources } : {},
|
|
32650
|
+
...typeof m.notes === "string" ? { notes: m.notes } : {}
|
|
32651
|
+
}
|
|
32652
|
+
};
|
|
32653
|
+
}
|
|
32654
|
+
function snapshotDir(root, id) {
|
|
32655
|
+
return join21(root, PAGES_DIR, id);
|
|
32656
|
+
}
|
|
32657
|
+
function writeSnapshot(root, snap) {
|
|
32658
|
+
const dir = snapshotDir(root, snap.meta.id);
|
|
32659
|
+
mkdirSync4(dir, { recursive: true });
|
|
32660
|
+
writeFileSync5(join21(dir, "meta.json"), `${JSON.stringify(snap.meta, null, 2)}
|
|
32661
|
+
`);
|
|
32662
|
+
const comment = formatCaptureComment({
|
|
32663
|
+
v: 1,
|
|
32664
|
+
page: snap.meta.id,
|
|
32665
|
+
url: snap.meta.url,
|
|
32666
|
+
...snap.meta.sources?.[0] ? { sourceFile: snap.meta.sources[0] } : {},
|
|
32667
|
+
name: snap.meta.name
|
|
32668
|
+
});
|
|
32669
|
+
writeFileSync5(join21(dir, "dom.html"), `${comment}
|
|
32670
|
+
${snap.dom}
|
|
32671
|
+
`);
|
|
32672
|
+
if (snap.styles) writeFileSync5(join21(dir, "styles.json"), `${JSON.stringify(snap.styles)}
|
|
32673
|
+
`);
|
|
32674
|
+
if (snap.boxes) writeFileSync5(join21(dir, "boxes.json"), `${JSON.stringify(snap.boxes)}
|
|
32675
|
+
`);
|
|
32676
|
+
if (snap.axtree) writeFileSync5(join21(dir, "axtree.json"), `${JSON.stringify(snap.axtree)}
|
|
32677
|
+
`);
|
|
32678
|
+
if (snap.css) writeFileSync5(join21(dir, "css.json"), `${JSON.stringify(snap.css)}
|
|
32679
|
+
`);
|
|
32680
|
+
return dir;
|
|
32681
|
+
}
|
|
32682
|
+
function readJson2(file) {
|
|
32683
|
+
try {
|
|
32684
|
+
return JSON.parse(readFileSync10(file, "utf8"));
|
|
32685
|
+
} catch {
|
|
32686
|
+
return void 0;
|
|
32687
|
+
}
|
|
32688
|
+
}
|
|
32689
|
+
function readSnapshot(dir) {
|
|
32690
|
+
const rawMeta = readJson2(join21(dir, "meta.json"));
|
|
32691
|
+
if (rawMeta === void 0) return null;
|
|
32692
|
+
const v = validateSnapshotMeta(rawMeta);
|
|
32693
|
+
if (!v.ok || !v.meta) return null;
|
|
32694
|
+
let dom;
|
|
32695
|
+
try {
|
|
32696
|
+
dom = readFileSync10(join21(dir, "dom.html"), "utf8");
|
|
32697
|
+
} catch {
|
|
32698
|
+
return null;
|
|
32699
|
+
}
|
|
32700
|
+
const styles = readJson2(join21(dir, "styles.json"));
|
|
32701
|
+
const boxes = readJson2(join21(dir, "boxes.json"));
|
|
32702
|
+
const axtree = readJson2(join21(dir, "axtree.json"));
|
|
32703
|
+
const css = readJson2(join21(dir, "css.json"));
|
|
32704
|
+
const shot = join21(dir, "screen.png");
|
|
32705
|
+
return {
|
|
32706
|
+
meta: v.meta,
|
|
32707
|
+
dom,
|
|
32708
|
+
...styles ? { styles } : {},
|
|
32709
|
+
...boxes ? { boxes } : {},
|
|
32710
|
+
...axtree ? { axtree } : {},
|
|
32711
|
+
...css ? { css } : {},
|
|
32712
|
+
...existsSync9(shot) ? { screenshot: "screen.png" } : {}
|
|
32713
|
+
};
|
|
32714
|
+
}
|
|
32715
|
+
function readSnapshots(root) {
|
|
32716
|
+
const base = join21(root, PAGES_DIR);
|
|
32717
|
+
let dirs;
|
|
32718
|
+
try {
|
|
32719
|
+
dirs = readdirSync4(base, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
32720
|
+
} catch {
|
|
32721
|
+
return [];
|
|
32722
|
+
}
|
|
32723
|
+
const out2 = [];
|
|
32724
|
+
for (const d of dirs.sort()) {
|
|
32725
|
+
const s = readSnapshot(join21(base, d));
|
|
32726
|
+
if (s) out2.push(s);
|
|
32727
|
+
}
|
|
32728
|
+
return out2;
|
|
32729
|
+
}
|
|
32730
|
+
var COLLECTED_CSS = [
|
|
32731
|
+
"color",
|
|
32732
|
+
"backgroundColor",
|
|
32733
|
+
"backgroundImage",
|
|
32734
|
+
"fontSize",
|
|
32735
|
+
"fontWeight",
|
|
32736
|
+
"fontStyle",
|
|
32737
|
+
"textDecorationLine",
|
|
32738
|
+
"textTransform",
|
|
32739
|
+
"lineHeight",
|
|
32740
|
+
"letterSpacing",
|
|
32741
|
+
"wordSpacing",
|
|
32742
|
+
"whiteSpace",
|
|
32743
|
+
"display",
|
|
32744
|
+
"visibility",
|
|
32745
|
+
"opacity",
|
|
32746
|
+
"position",
|
|
32747
|
+
"overflowX",
|
|
32748
|
+
"overflowY",
|
|
32749
|
+
"outlineStyle",
|
|
32750
|
+
"outlineWidth",
|
|
32751
|
+
"outlineColor",
|
|
32752
|
+
// All four borders + the box shadow: WCAG 1.4.11 asks whether a control's BOUNDARY is
|
|
32753
|
+
// perceivable, and a boundary can be drawn by any side, by the fill, by an outline or by a
|
|
32754
|
+
// shadow. Collecting only one side would manufacture non-conformities on the other three.
|
|
32755
|
+
"borderTopStyle",
|
|
32756
|
+
"borderRightStyle",
|
|
32757
|
+
"borderBottomStyle",
|
|
32758
|
+
"borderLeftStyle",
|
|
32759
|
+
"borderTopWidth",
|
|
32760
|
+
"borderRightWidth",
|
|
32761
|
+
"borderBottomWidth",
|
|
32762
|
+
"borderLeftWidth",
|
|
32763
|
+
"borderTopColor",
|
|
32764
|
+
"borderRightColor",
|
|
32765
|
+
"borderBottomColor",
|
|
32766
|
+
"borderLeftColor",
|
|
32767
|
+
"boxShadow",
|
|
32768
|
+
"cursor"
|
|
32769
|
+
];
|
|
32770
|
+
var COLLECT_MAX_ELEMENTS = 5e3;
|
|
32771
|
+
var COLLECT_SNAPSHOT = `(() => {
|
|
32772
|
+
const PROPS = ${JSON.stringify(COLLECTED_CSS)};
|
|
32773
|
+
const MAX = ${COLLECT_MAX_ELEMENTS};
|
|
32774
|
+
const els = document.querySelectorAll('*');
|
|
32775
|
+
const styles = [];
|
|
32776
|
+
const boxes = [];
|
|
32777
|
+
const n = Math.min(els.length, MAX);
|
|
32778
|
+
for (let i = 0; i < n; i++) {
|
|
32779
|
+
const el = els[i];
|
|
32780
|
+
const tag = el.tagName.toLowerCase();
|
|
32781
|
+
const cs = getComputedStyle(el);
|
|
32782
|
+
const css = {};
|
|
32783
|
+
for (const p of PROPS) {
|
|
32784
|
+
const v = cs[p];
|
|
32785
|
+
if (v !== undefined && v !== null && v !== '') css[p] = String(v);
|
|
32786
|
+
}
|
|
32787
|
+
styles.push({ i: i, tag: tag, css: css });
|
|
32788
|
+
const r = el.getBoundingClientRect();
|
|
32789
|
+
boxes.push({ i: i, tag: tag, x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) });
|
|
32790
|
+
}
|
|
32791
|
+
// The page's own stylesheets. Some criteria are properties of the STYLESHEET rather than of
|
|
32792
|
+
// any element's computed style \u2014 focus styling removed with no replacement (2.4.7), a media
|
|
32793
|
+
// query that locks the orientation (1.3.4) \u2014 and this is the only place they exist.
|
|
32794
|
+
// A cross-origin sheet throws on .cssRules: COUNT those instead of ignoring them, so a rule
|
|
32795
|
+
// reading this digest can tell "nothing there" apart from "I could not look".
|
|
32796
|
+
const cssRules = [];
|
|
32797
|
+
let unreadable = 0;
|
|
32798
|
+
const MAXR = 4000;
|
|
32799
|
+
const camel = (p) => p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
32800
|
+
const walk = (list, media) => {
|
|
32801
|
+
for (const r of list) {
|
|
32802
|
+
if (cssRules.length >= MAXR) return;
|
|
32803
|
+
if (typeof r.selectorText === 'string' && r.style) {
|
|
32804
|
+
const decls = {};
|
|
32805
|
+
for (let i = 0; i < r.style.length; i++) {
|
|
32806
|
+
const prop = r.style[i];
|
|
32807
|
+
decls[camel(prop)] = r.style.getPropertyValue(prop);
|
|
32808
|
+
}
|
|
32809
|
+
cssRules.push(media ? { selector: r.selectorText, media: media, decls: decls } : { selector: r.selectorText, decls: decls });
|
|
32810
|
+
} else if (r.cssRules) {
|
|
32811
|
+
const cond = r.conditionText || (r.media && r.media.mediaText) || media;
|
|
32812
|
+
walk(r.cssRules, cond);
|
|
32813
|
+
}
|
|
32814
|
+
}
|
|
32815
|
+
};
|
|
32816
|
+
for (const sheet of document.styleSheets) {
|
|
32817
|
+
try {
|
|
32818
|
+
walk(sheet.cssRules, undefined);
|
|
32819
|
+
} catch (e) {
|
|
32820
|
+
unreadable++;
|
|
32821
|
+
}
|
|
32822
|
+
}
|
|
32823
|
+
|
|
32824
|
+
const truncated = els.length > MAX;
|
|
32825
|
+
return {
|
|
32826
|
+
dom: document.documentElement.outerHTML,
|
|
32827
|
+
css: { v: 1, rules: cssRules, unreadable: unreadable, truncated: cssRules.length >= MAXR },
|
|
32828
|
+
title: document.title,
|
|
32829
|
+
lang: document.documentElement.getAttribute('lang') || '',
|
|
32830
|
+
url: location.href,
|
|
32831
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
32832
|
+
styles: { v: 1, entries: styles, truncated: truncated },
|
|
32833
|
+
boxes: { v: 1, entries: boxes, truncated: truncated }
|
|
32834
|
+
};
|
|
32835
|
+
})()`;
|
|
32836
|
+
function isSnapshotDom(file) {
|
|
32837
|
+
const posix3 = file.split("\\").join("/");
|
|
32838
|
+
return posix3.endsWith("/dom.html") && posix3.includes(`${PAGES_DIR}/`);
|
|
32839
|
+
}
|
|
32840
|
+
function align(doc, entries) {
|
|
32841
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
32842
|
+
for (const e of entries) {
|
|
32843
|
+
const el = doc.elements[e.i];
|
|
32844
|
+
if (!el || el.tag !== e.tag) return null;
|
|
32845
|
+
out2.set(e.i, e);
|
|
32846
|
+
}
|
|
32847
|
+
return out2;
|
|
32848
|
+
}
|
|
32849
|
+
function attachSignals(doc) {
|
|
32850
|
+
if (!isSnapshotDom(doc.file)) return;
|
|
32851
|
+
const dir = dirname5(doc.file);
|
|
32852
|
+
const styles = readJson2(join21(dir, "styles.json"));
|
|
32853
|
+
const boxes = readJson2(join21(dir, "boxes.json"));
|
|
32854
|
+
const axtree = readJson2(join21(dir, "axtree.json"));
|
|
32855
|
+
const css = readJson2(join21(dir, "css.json"));
|
|
32856
|
+
const shot = join21(dir, "screen.png");
|
|
32857
|
+
const alignedStyleMap = styles ? align(doc, styles.entries) : null;
|
|
32858
|
+
const alignedBoxMap = boxes ? align(doc, boxes.entries) : null;
|
|
32859
|
+
const truncated = Boolean(styles?.truncated || boxes?.truncated);
|
|
32860
|
+
const signals = {
|
|
32861
|
+
...alignedStyleMap ? { styles: alignedStyleMap } : {},
|
|
32862
|
+
...alignedBoxMap ? { boxes: alignedBoxMap } : {},
|
|
32863
|
+
...axtree ? { axtree } : {},
|
|
32864
|
+
// Not element-indexed, so it needs no alignment — it is a property of the stylesheet.
|
|
32865
|
+
...css ? { css } : {},
|
|
32866
|
+
...existsSync9(shot) ? { screenshot: shot } : {},
|
|
32867
|
+
...truncated ? { truncated } : {}
|
|
32868
|
+
};
|
|
32869
|
+
if (Object.keys(signals).length) doc.signals = signals;
|
|
32870
|
+
}
|
|
32871
|
+
|
|
32582
32872
|
// src/messages.ts
|
|
32583
32873
|
var MSG_CATALOG = {
|
|
32584
32874
|
// ---- Theme 1 — Images (src/rules/images.ts) ---------------------------------
|
|
@@ -32824,6 +33114,69 @@ var MSG_CATALOG = {
|
|
|
32824
33114
|
en: (p) => `Darken the text or lighten the background to reach at least ${p.min}:1 (contrast computed from literal inline colours).`
|
|
32825
33115
|
}
|
|
32826
33116
|
},
|
|
33117
|
+
// ---- Rendered tier (src/rules/rendered.ts) — decided from a page snapshot's signals -----
|
|
33118
|
+
// These say WHERE the measurement came from, because that is what makes them auditable: a
|
|
33119
|
+
// computed style, or a region of the screenshot. An auditor can go and re-check either.
|
|
33120
|
+
"rendered-contrast": {
|
|
33121
|
+
message: {
|
|
33122
|
+
fr: (p) => `Contraste insuffisant au rendu : ratio ${p.ratio}:1 entre le texte et son fond effectif (minimum ${p.min}:1 pour du texte ${p.textSize === "large" ? "large" : "normal"}).`,
|
|
33123
|
+
en: (p) => `Insufficient contrast at render: ${p.ratio}:1 between the text and its effective background (minimum ${p.min}:1 for ${p.textSize === "large" ? "large" : "normal"} text).`
|
|
33124
|
+
},
|
|
33125
|
+
remediation: {
|
|
33126
|
+
fr: (p) => `Assombrissez le texte ou \xE9claircissez le fond pour atteindre au moins ${p.min}:1 (mesur\xE9 sur les styles calcul\xE9s de la page rendue).`,
|
|
33127
|
+
en: (p) => `Darken the text or lighten the background to reach at least ${p.min}:1 (measured from the rendered page's computed styles).`
|
|
33128
|
+
}
|
|
33129
|
+
},
|
|
33130
|
+
"rendered-contrast-pixel": {
|
|
33131
|
+
message: {
|
|
33132
|
+
fr: (p) => `Contraste insuffisant mesur\xE9 sur la capture d'\xE9cran : ratio ${p.ratio}:1 (minimum ${p.min}:1 pour du texte ${p.textSize === "large" ? "large" : "normal"}). Le fond vient d'une image ou d'un d\xE9grad\xE9, invisible aux styles calcul\xE9s.`,
|
|
33133
|
+
en: (p) => `Insufficient contrast measured on the screenshot: ${p.ratio}:1 (minimum ${p.min}:1 for ${p.textSize === "large" ? "large" : "normal"} text). The background comes from an image or gradient, which computed styles cannot express.`
|
|
33134
|
+
},
|
|
33135
|
+
remediation: {
|
|
33136
|
+
fr: (p) => `Garantissez au moins ${p.min}:1 sur toute la zone : ajoutez un fond opaque sous le texte, un voile, ou une ombre port\xE9e. Le fond a \xE9t\xE9 mesur\xE9 sur les pixels de la zone occup\xE9e par le texte.`,
|
|
33137
|
+
en: (p) => `Guarantee at least ${p.min}:1 across the whole area: add an opaque backing behind the text, a scrim, or a text shadow. The background was measured on the pixels of the text's own box.`
|
|
33138
|
+
}
|
|
33139
|
+
},
|
|
33140
|
+
"rendered-link-colour-only": {
|
|
33141
|
+
message: {
|
|
33142
|
+
fr: (p) => `Lien dans un bloc de texte distingu\xE9 par la COULEUR SEULE (\xE9cart de ${p.ratio}:1 avec le texte environnant, sans soulignement, bordure ni fond) \u2014 invisible pour qui ne per\xE7oit pas cette diff\xE9rence de couleur.`,
|
|
33143
|
+
en: (p) => `Link inside a text block distinguished by COLOUR ALONE (${p.ratio}:1 apart from the surrounding text, with no underline, border or background) \u2014 invisible to anyone who does not perceive that colour difference.`
|
|
33144
|
+
},
|
|
33145
|
+
remediation: {
|
|
33146
|
+
fr: () => `Ajoutez un indice non chromatique : text-decoration: underline (le plus simple), une bordure basse, ou une graisse nettement sup\xE9rieure. \xC0 d\xE9faut, un contraste d'au moins 3:1 avec le texte environnant ET un indice au survol/focus.`,
|
|
33147
|
+
en: () => `Add a non-colour cue: text-decoration: underline (simplest), a bottom border, or a distinctly heavier weight. Failing that, at least 3:1 contrast with the surrounding text AND a cue on hover/focus.`
|
|
33148
|
+
}
|
|
33149
|
+
},
|
|
33150
|
+
"rendered-nontext-contrast": {
|
|
33151
|
+
message: {
|
|
33152
|
+
fr: (p) => `Limite du composant <${p.control}> non perceptible : ni son fond (${p.ratio}:1 avec l'environnement) ni sa bordure n'atteignent 3:1 \u2014 rien n'indique visuellement o\xF9 commence le champ.`,
|
|
33153
|
+
en: (p) => `The <${p.control}> boundary is not perceivable: neither its fill (${p.ratio}:1 against its surroundings) nor its border reaches 3:1 \u2014 nothing shows visually where the control begins.`
|
|
33154
|
+
},
|
|
33155
|
+
remediation: {
|
|
33156
|
+
fr: () => `Donnez au composant une bordure visible contrastant \xE0 3:1 avec le fond environnant, ou un fond distinct du fond environnant \xE0 3:1. Un box-shadow ou un outline visible conviennent aussi.`,
|
|
33157
|
+
en: () => `Give the control a visible border at 3:1 against the surrounding background, or a fill distinct from it at 3:1. A visible box-shadow or outline also qualifies.`
|
|
33158
|
+
}
|
|
33159
|
+
},
|
|
33160
|
+
"rendered-focus-not-visible": {
|
|
33161
|
+
message: {
|
|
33162
|
+
fr: (p) => `La feuille de styles supprime l'indicateur de focus (\`${p.selector}\`) et aucune r\xE8gle \`:focus\` ne le remplace \u2014 un utilisateur au clavier ne peut plus savoir o\xF9 il se trouve.`,
|
|
33163
|
+
en: (p) => `The stylesheet removes the focus indicator (\`${p.selector}\`) and no \`:focus\` rule puts one back \u2014 a keyboard user can no longer tell where they are.`
|
|
33164
|
+
},
|
|
33165
|
+
remediation: {
|
|
33166
|
+
fr: () => `Ne supprimez jamais outline sans remplacement. Si le style par d\xE9faut ne convient pas, remplacez-le : \`:focus-visible { outline: 2px solid <couleur contrast\xE9e>; outline-offset: 2px }\`.`,
|
|
33167
|
+
en: () => `Never remove outline without a replacement. If the default does not fit, replace it: \`:focus-visible { outline: 2px solid <contrasting colour>; outline-offset: 2px }\`.`
|
|
33168
|
+
}
|
|
33169
|
+
},
|
|
33170
|
+
"rendered-orientation-lock": {
|
|
33171
|
+
message: {
|
|
33172
|
+
fr: (p) => `Le contenu est verrouill\xE9 dans une seule orientation : \`@media ${p.media}\` applique \`transform: ${p.transform}\` au document \u2014 l'utilisateur ne peut pas consulter la page dans l'orientation de son choix.`,
|
|
33173
|
+
en: (p) => `Content is locked to a single orientation: \`@media ${p.media}\` applies \`transform: ${p.transform}\` to the document \u2014 the user cannot view the page in the orientation they need.`
|
|
33174
|
+
},
|
|
33175
|
+
remediation: {
|
|
33176
|
+
fr: () => `Supprimez la rotation et rendez la mise en page responsive dans les deux orientations. Un verrouillage n'est admis que si l'orientation est essentielle (piano, ch\xE8que bancaire).`,
|
|
33177
|
+
en: () => `Remove the rotation and make the layout work in both orientations. Locking is only acceptable where the orientation is essential (a piano keyboard, a bank cheque).`
|
|
33178
|
+
}
|
|
33179
|
+
},
|
|
32827
33180
|
// ---- Theme 5 — Data tables (src/rules/tables.ts) ----------------------------
|
|
32828
33181
|
"data-table-no-headers.no-th": {
|
|
32829
33182
|
message: {
|
|
@@ -33566,6 +33919,9 @@ function toFinding(doc, ruleId, def, rf, ruleAdvisory) {
|
|
|
33566
33919
|
// Capture findings are rendered ground truth (NOT preliminary): re-attribute them
|
|
33567
33920
|
// to the source component recorded in the capture's provenance.
|
|
33568
33921
|
...doc.capture ? { origin: { capture: doc.file, sourceFile: doc.capture.sourceFile, component: doc.capture.component } } : {},
|
|
33922
|
+
// A PAGE snapshot's capture carries which page it is; stamp it so the per-page grid can
|
|
33923
|
+
// key on the finding directly (src/pages.ts). Component captures carry no page.
|
|
33924
|
+
...doc.capture?.page ? { page: doc.capture.page } : {},
|
|
33569
33925
|
// Non-normative recommendation: the per-finding override (rf.advisory) wins over the
|
|
33570
33926
|
// rule-level default (ruleAdvisory); only stamp the flag when it is actually true, so
|
|
33571
33927
|
// a normative finding stays byte-identical (no `advisory: false` noise in the JSON).
|
|
@@ -36282,6 +36638,422 @@ var contrastLiteral = {
|
|
|
36282
36638
|
};
|
|
36283
36639
|
var colorsRules = [contrastLiteral];
|
|
36284
36640
|
|
|
36641
|
+
// src/rules/rendered.ts
|
|
36642
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
36643
|
+
|
|
36644
|
+
// src/pixel.ts
|
|
36645
|
+
import { inflateSync } from "zlib";
|
|
36646
|
+
var SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
36647
|
+
function paeth(a, b, c2) {
|
|
36648
|
+
const p = a + b - c2;
|
|
36649
|
+
const pa = Math.abs(p - a);
|
|
36650
|
+
const pb = Math.abs(p - b);
|
|
36651
|
+
const pc = Math.abs(p - c2);
|
|
36652
|
+
if (pa <= pb && pa <= pc) return a;
|
|
36653
|
+
return pb <= pc ? b : c2;
|
|
36654
|
+
}
|
|
36655
|
+
function decodePng(buf) {
|
|
36656
|
+
if (buf.length < 8 + 25) return null;
|
|
36657
|
+
for (let i2 = 0; i2 < SIGNATURE.length; i2++) if (buf[i2] !== SIGNATURE[i2]) return null;
|
|
36658
|
+
let width = 0;
|
|
36659
|
+
let height = 0;
|
|
36660
|
+
let bitDepth = 0;
|
|
36661
|
+
let colorType = 0;
|
|
36662
|
+
let interlace = 0;
|
|
36663
|
+
const idat = [];
|
|
36664
|
+
let off = 8;
|
|
36665
|
+
while (off + 8 <= buf.length) {
|
|
36666
|
+
const len = buf.readUInt32BE(off);
|
|
36667
|
+
const type = buf.toString("ascii", off + 4, off + 8);
|
|
36668
|
+
const dataStart = off + 8;
|
|
36669
|
+
const dataEnd = dataStart + len;
|
|
36670
|
+
if (dataEnd + 4 > buf.length) return null;
|
|
36671
|
+
if (type === "IHDR") {
|
|
36672
|
+
if (len < 13) return null;
|
|
36673
|
+
width = buf.readUInt32BE(dataStart);
|
|
36674
|
+
height = buf.readUInt32BE(dataStart + 4);
|
|
36675
|
+
bitDepth = buf[dataStart + 8] ?? 0;
|
|
36676
|
+
colorType = buf[dataStart + 9] ?? 0;
|
|
36677
|
+
interlace = buf[dataStart + 12] ?? 0;
|
|
36678
|
+
} else if (type === "IDAT") {
|
|
36679
|
+
idat.push(buf.subarray(dataStart, dataEnd));
|
|
36680
|
+
} else if (type === "IEND") {
|
|
36681
|
+
break;
|
|
36682
|
+
}
|
|
36683
|
+
off = dataEnd + 4;
|
|
36684
|
+
}
|
|
36685
|
+
if (!width || !height || !idat.length) return null;
|
|
36686
|
+
if (bitDepth !== 8) return null;
|
|
36687
|
+
if (colorType !== 2 && colorType !== 6) return null;
|
|
36688
|
+
if (interlace !== 0) return null;
|
|
36689
|
+
const channels = colorType === 6 ? 4 : 3;
|
|
36690
|
+
let raw;
|
|
36691
|
+
try {
|
|
36692
|
+
raw = inflateSync(Buffer.concat(idat));
|
|
36693
|
+
} catch {
|
|
36694
|
+
return null;
|
|
36695
|
+
}
|
|
36696
|
+
const stride = width * channels;
|
|
36697
|
+
if (raw.length < height * (stride + 1)) return null;
|
|
36698
|
+
const out2 = Buffer.alloc(height * stride);
|
|
36699
|
+
for (let y = 0; y < height; y++) {
|
|
36700
|
+
const filter = raw[y * (stride + 1)] ?? 0;
|
|
36701
|
+
const lineIn = y * (stride + 1) + 1;
|
|
36702
|
+
const lineOut = y * stride;
|
|
36703
|
+
const prevOut = lineOut - stride;
|
|
36704
|
+
for (let i2 = 0; i2 < stride; i2++) {
|
|
36705
|
+
const x = raw[lineIn + i2] ?? 0;
|
|
36706
|
+
const a = i2 >= channels ? out2[lineOut + i2 - channels] ?? 0 : 0;
|
|
36707
|
+
const b = y > 0 ? out2[prevOut + i2] ?? 0 : 0;
|
|
36708
|
+
const c2 = y > 0 && i2 >= channels ? out2[prevOut + i2 - channels] ?? 0 : 0;
|
|
36709
|
+
let v;
|
|
36710
|
+
switch (filter) {
|
|
36711
|
+
case 0:
|
|
36712
|
+
v = x;
|
|
36713
|
+
break;
|
|
36714
|
+
case 1:
|
|
36715
|
+
v = x + a;
|
|
36716
|
+
break;
|
|
36717
|
+
case 2:
|
|
36718
|
+
v = x + b;
|
|
36719
|
+
break;
|
|
36720
|
+
case 3:
|
|
36721
|
+
v = x + (a + b >> 1);
|
|
36722
|
+
break;
|
|
36723
|
+
case 4:
|
|
36724
|
+
v = x + paeth(a, b, c2);
|
|
36725
|
+
break;
|
|
36726
|
+
default:
|
|
36727
|
+
return null;
|
|
36728
|
+
}
|
|
36729
|
+
out2[lineOut + i2] = v & 255;
|
|
36730
|
+
}
|
|
36731
|
+
}
|
|
36732
|
+
return {
|
|
36733
|
+
width,
|
|
36734
|
+
height,
|
|
36735
|
+
at(x, y) {
|
|
36736
|
+
if (x < 0 || y < 0 || x >= width || y >= height) return void 0;
|
|
36737
|
+
const p = y * stride + x * channels;
|
|
36738
|
+
return {
|
|
36739
|
+
r: out2[p] ?? 0,
|
|
36740
|
+
g: out2[p + 1] ?? 0,
|
|
36741
|
+
b: out2[p + 2] ?? 0,
|
|
36742
|
+
a: channels === 4 ? (out2[p + 3] ?? 255) / 255 : 1
|
|
36743
|
+
};
|
|
36744
|
+
}
|
|
36745
|
+
};
|
|
36746
|
+
}
|
|
36747
|
+
function clamp(img, r) {
|
|
36748
|
+
const x = Math.max(0, Math.round(r.x));
|
|
36749
|
+
const y = Math.max(0, Math.round(r.y));
|
|
36750
|
+
const x2 = Math.min(img.width, Math.round(r.x + r.w));
|
|
36751
|
+
const y2 = Math.min(img.height, Math.round(r.y + r.h));
|
|
36752
|
+
if (x2 <= x || y2 <= y) return null;
|
|
36753
|
+
return { x, y, w: x2 - x, h: y2 - y };
|
|
36754
|
+
}
|
|
36755
|
+
var MAX_SAMPLES = 4096;
|
|
36756
|
+
var DOMINANCE = 0.6;
|
|
36757
|
+
var BUCKET = 16;
|
|
36758
|
+
function dominantBackground(img, rect) {
|
|
36759
|
+
const r = clamp(img, rect);
|
|
36760
|
+
if (!r) return null;
|
|
36761
|
+
const step = Math.max(1, Math.floor(Math.sqrt(r.w * r.h / MAX_SAMPLES)));
|
|
36762
|
+
const counts = /* @__PURE__ */ new Map();
|
|
36763
|
+
let total = 0;
|
|
36764
|
+
for (let y = r.y; y < r.y + r.h; y += step) {
|
|
36765
|
+
for (let x = r.x; x < r.x + r.w; x += step) {
|
|
36766
|
+
const p = img.at(x, y);
|
|
36767
|
+
if (!p) continue;
|
|
36768
|
+
const key = Math.round(p.r / BUCKET) << 16 | Math.round(p.g / BUCKET) << 8 | Math.round(p.b / BUCKET);
|
|
36769
|
+
const cur = counts.get(key) ?? { n: 0, r: 0, g: 0, b: 0 };
|
|
36770
|
+
cur.n++;
|
|
36771
|
+
cur.r += p.r;
|
|
36772
|
+
cur.g += p.g;
|
|
36773
|
+
cur.b += p.b;
|
|
36774
|
+
counts.set(key, cur);
|
|
36775
|
+
total++;
|
|
36776
|
+
}
|
|
36777
|
+
}
|
|
36778
|
+
if (!total) return null;
|
|
36779
|
+
let best = null;
|
|
36780
|
+
for (const c2 of counts.values()) if (!best || c2.n > best.n) best = c2;
|
|
36781
|
+
if (!best || best.n / total < DOMINANCE) return null;
|
|
36782
|
+
return { r: Math.round(best.r / best.n), g: Math.round(best.g / best.n), b: Math.round(best.b / best.n), a: 1 };
|
|
36783
|
+
}
|
|
36784
|
+
|
|
36785
|
+
// src/rules/rendered.ts
|
|
36786
|
+
var SKIP_TAGS2 = /* @__PURE__ */ new Set(["script", "style", "head", "title", "meta", "noscript", "link", "html", "br", "wbr"]);
|
|
36787
|
+
function styleAt(doc, i2) {
|
|
36788
|
+
return doc.signals?.styles?.get(i2);
|
|
36789
|
+
}
|
|
36790
|
+
function hasDirectText2(el) {
|
|
36791
|
+
return el.children.some((c2) => c2.type === "text" && c2.data.trim() !== "");
|
|
36792
|
+
}
|
|
36793
|
+
function invisible(css) {
|
|
36794
|
+
if (css.display === "none" || css.visibility === "hidden") return true;
|
|
36795
|
+
const op = Number.parseFloat(css.opacity ?? "1");
|
|
36796
|
+
return Number.isFinite(op) && op === 0;
|
|
36797
|
+
}
|
|
36798
|
+
function backdropOf(doc, index, el) {
|
|
36799
|
+
for (let p = el; p; p = p.parent) {
|
|
36800
|
+
const i2 = index.get(p);
|
|
36801
|
+
if (i2 === void 0) continue;
|
|
36802
|
+
const css = styleAt(doc, i2)?.css;
|
|
36803
|
+
if (!css) continue;
|
|
36804
|
+
const img = css.backgroundImage;
|
|
36805
|
+
if (img && img !== "none") return void 0;
|
|
36806
|
+
const c2 = parseColor(css.backgroundColor ?? "");
|
|
36807
|
+
if (c2 && c2.a >= 1) return { color: c2, fromImage: false };
|
|
36808
|
+
}
|
|
36809
|
+
return void 0;
|
|
36810
|
+
}
|
|
36811
|
+
function isLargeText(css) {
|
|
36812
|
+
const px = Number.parseFloat(css.fontSize ?? "");
|
|
36813
|
+
if (!Number.isFinite(px)) return false;
|
|
36814
|
+
const weight = Number.parseInt(css.fontWeight ?? "400", 10);
|
|
36815
|
+
const bold = Number.isFinite(weight) ? weight >= 700 : css.fontWeight === "bold";
|
|
36816
|
+
return px >= 24 || px >= 18.66 && bold;
|
|
36817
|
+
}
|
|
36818
|
+
function elementIndex(doc) {
|
|
36819
|
+
const m = /* @__PURE__ */ new Map();
|
|
36820
|
+
doc.elements.forEach((el, i2) => m.set(el, i2));
|
|
36821
|
+
return m;
|
|
36822
|
+
}
|
|
36823
|
+
var renderedContrast = {
|
|
36824
|
+
id: "rendered-contrast",
|
|
36825
|
+
criteria: ["1.4.3"],
|
|
36826
|
+
severity: "majeur",
|
|
36827
|
+
run(doc) {
|
|
36828
|
+
if (!doc.signals?.styles) return [];
|
|
36829
|
+
const index = elementIndex(doc);
|
|
36830
|
+
const out2 = [];
|
|
36831
|
+
for (const [el, i2] of index) {
|
|
36832
|
+
if (SKIP_TAGS2.has(el.tag) || !hasDirectText2(el)) continue;
|
|
36833
|
+
const css = styleAt(doc, i2)?.css;
|
|
36834
|
+
if (!css || invisible(css)) continue;
|
|
36835
|
+
const fg = parseColor(css.color ?? "");
|
|
36836
|
+
if (!fg || fg.a < 1) continue;
|
|
36837
|
+
const bd = backdropOf(doc, index, el);
|
|
36838
|
+
if (!bd) continue;
|
|
36839
|
+
const ratio = contrastRatio(fg, bd.color);
|
|
36840
|
+
const large = isLargeText(css);
|
|
36841
|
+
const min = large ? 3 : 4.5;
|
|
36842
|
+
if (ratio >= min) continue;
|
|
36843
|
+
out2.push({
|
|
36844
|
+
criteriaId: "1.4.3",
|
|
36845
|
+
el,
|
|
36846
|
+
msgId: "rendered-contrast",
|
|
36847
|
+
params: { ratio: ratio.toFixed(2), min, textSize: large ? "large" : "normal" }
|
|
36848
|
+
});
|
|
36849
|
+
}
|
|
36850
|
+
return out2;
|
|
36851
|
+
}
|
|
36852
|
+
};
|
|
36853
|
+
var renderedContrastPixel = {
|
|
36854
|
+
id: "rendered-contrast-pixel",
|
|
36855
|
+
criteria: ["1.4.3"],
|
|
36856
|
+
severity: "majeur",
|
|
36857
|
+
run(doc) {
|
|
36858
|
+
const shot = doc.signals?.screenshot;
|
|
36859
|
+
const boxes = doc.signals?.boxes;
|
|
36860
|
+
if (!shot || !boxes || !doc.signals?.styles) return [];
|
|
36861
|
+
let img = null;
|
|
36862
|
+
try {
|
|
36863
|
+
img = decodePng(readFileSync11(shot));
|
|
36864
|
+
} catch {
|
|
36865
|
+
return [];
|
|
36866
|
+
}
|
|
36867
|
+
if (!img) return [];
|
|
36868
|
+
const index = elementIndex(doc);
|
|
36869
|
+
const out2 = [];
|
|
36870
|
+
for (const [el, i2] of index) {
|
|
36871
|
+
if (SKIP_TAGS2.has(el.tag) || !hasDirectText2(el)) continue;
|
|
36872
|
+
const css = styleAt(doc, i2)?.css;
|
|
36873
|
+
if (!css || invisible(css)) continue;
|
|
36874
|
+
if (backdropOf(doc, index, el)) continue;
|
|
36875
|
+
const fg = parseColor(css.color ?? "");
|
|
36876
|
+
if (!fg || fg.a < 1) continue;
|
|
36877
|
+
const box = boxes.get(i2);
|
|
36878
|
+
if (!box || box.w < 4 || box.h < 4) continue;
|
|
36879
|
+
const bg = dominantBackground(img, box);
|
|
36880
|
+
if (!bg) continue;
|
|
36881
|
+
const ratio = contrastRatio(fg, bg);
|
|
36882
|
+
const large = isLargeText(css);
|
|
36883
|
+
const min = large ? 3 : 4.5;
|
|
36884
|
+
if (ratio >= min) continue;
|
|
36885
|
+
out2.push({
|
|
36886
|
+
criteriaId: "1.4.3",
|
|
36887
|
+
el,
|
|
36888
|
+
msgId: "rendered-contrast-pixel",
|
|
36889
|
+
params: { ratio: ratio.toFixed(2), min, textSize: large ? "large" : "normal" }
|
|
36890
|
+
});
|
|
36891
|
+
}
|
|
36892
|
+
return out2;
|
|
36893
|
+
}
|
|
36894
|
+
};
|
|
36895
|
+
function hasNonColourAffordance(css, parentCss) {
|
|
36896
|
+
const deco = css.textDecorationLine ?? "";
|
|
36897
|
+
if (deco && deco !== "none") return true;
|
|
36898
|
+
if ((css.borderBottomStyle ?? "none") !== "none" && Number.parseFloat(css.borderBottomWidth ?? "0") > 0) return true;
|
|
36899
|
+
const bg = parseColor(css.backgroundColor ?? "");
|
|
36900
|
+
if (bg && bg.a > 0) return true;
|
|
36901
|
+
const w = Number.parseInt(css.fontWeight ?? "400", 10);
|
|
36902
|
+
const pw = Number.parseInt(parentCss?.fontWeight ?? "400", 10);
|
|
36903
|
+
if (Number.isFinite(w) && Number.isFinite(pw) && w >= pw + 200) return true;
|
|
36904
|
+
return false;
|
|
36905
|
+
}
|
|
36906
|
+
var TEXT_BLOCK = /* @__PURE__ */ new Set(["p", "li", "dd", "dt", "td", "th", "blockquote", "figcaption", "caption"]);
|
|
36907
|
+
var renderedLinkColourOnly = {
|
|
36908
|
+
id: "rendered-link-colour-only",
|
|
36909
|
+
criteria: ["1.4.1"],
|
|
36910
|
+
severity: "majeur",
|
|
36911
|
+
run(doc) {
|
|
36912
|
+
if (!doc.signals?.styles) return [];
|
|
36913
|
+
const index = elementIndex(doc);
|
|
36914
|
+
const out2 = [];
|
|
36915
|
+
for (const [el, i2] of index) {
|
|
36916
|
+
if (el.tag !== "a" || !hasDirectText2(el)) continue;
|
|
36917
|
+
const parent = el.parent;
|
|
36918
|
+
if (!parent || !TEXT_BLOCK.has(parent.tag)) continue;
|
|
36919
|
+
const css = styleAt(doc, i2)?.css;
|
|
36920
|
+
const pi = index.get(parent);
|
|
36921
|
+
const parentCss = pi === void 0 ? void 0 : styleAt(doc, pi)?.css;
|
|
36922
|
+
if (!css || !parentCss || invisible(css)) continue;
|
|
36923
|
+
if (hasNonColourAffordance(css, parentCss)) continue;
|
|
36924
|
+
const linkColor = parseColor(css.color ?? "");
|
|
36925
|
+
const textColor = parseColor(parentCss.color ?? "");
|
|
36926
|
+
if (!linkColor || !textColor) continue;
|
|
36927
|
+
const distinct = contrastRatio(linkColor, textColor);
|
|
36928
|
+
if (distinct <= 1.02) continue;
|
|
36929
|
+
out2.push({
|
|
36930
|
+
criteriaId: "1.4.1",
|
|
36931
|
+
el,
|
|
36932
|
+
msgId: "rendered-link-colour-only",
|
|
36933
|
+
params: { ratio: distinct.toFixed(2) }
|
|
36934
|
+
});
|
|
36935
|
+
}
|
|
36936
|
+
return out2;
|
|
36937
|
+
}
|
|
36938
|
+
};
|
|
36939
|
+
var BOUNDED_CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea", "button"]);
|
|
36940
|
+
var SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
36941
|
+
function borderColours(css) {
|
|
36942
|
+
const out2 = [];
|
|
36943
|
+
for (const s of SIDES) {
|
|
36944
|
+
const style = css[`border${s}Style`];
|
|
36945
|
+
const width = Number.parseFloat(css[`border${s}Width`] ?? "0");
|
|
36946
|
+
if (!style || style === "none" || style === "hidden" || !(width > 0)) continue;
|
|
36947
|
+
const c2 = parseColor(css[`border${s}Color`] ?? "");
|
|
36948
|
+
if (c2 && c2.a >= 1) out2.push(c2);
|
|
36949
|
+
}
|
|
36950
|
+
return out2;
|
|
36951
|
+
}
|
|
36952
|
+
var renderedNonTextContrast = {
|
|
36953
|
+
id: "rendered-nontext-contrast",
|
|
36954
|
+
criteria: ["1.4.11"],
|
|
36955
|
+
severity: "majeur",
|
|
36956
|
+
run(doc) {
|
|
36957
|
+
if (!doc.signals?.styles) return [];
|
|
36958
|
+
const index = elementIndex(doc);
|
|
36959
|
+
const out2 = [];
|
|
36960
|
+
for (const [el, i2] of index) {
|
|
36961
|
+
if (!BOUNDED_CONTROLS.has(el.tag)) continue;
|
|
36962
|
+
const type = (el.attribs.type ?? "").toLowerCase();
|
|
36963
|
+
if (el.tag === "input" && (type === "hidden" || type === "image")) continue;
|
|
36964
|
+
const css = styleAt(doc, i2)?.css;
|
|
36965
|
+
if (!css || invisible(css)) continue;
|
|
36966
|
+
if (css.boxShadow && css.boxShadow !== "none") continue;
|
|
36967
|
+
if ((css.outlineStyle ?? "none") !== "none" && Number.parseFloat(css.outlineWidth ?? "0") > 0) continue;
|
|
36968
|
+
const surrounding = el.parent ? backdropOf(doc, index, el.parent) : void 0;
|
|
36969
|
+
if (!surrounding) continue;
|
|
36970
|
+
const own = parseColor(css.backgroundColor ?? "");
|
|
36971
|
+
const fill = own && own.a >= 1 ? own : surrounding.color;
|
|
36972
|
+
if (contrastRatio(fill, surrounding.color) >= 3) continue;
|
|
36973
|
+
const borders = borderColours(css);
|
|
36974
|
+
if (borders.some((b) => contrastRatio(b, surrounding.color) >= 3 || contrastRatio(b, fill) >= 3)) continue;
|
|
36975
|
+
out2.push({
|
|
36976
|
+
criteriaId: "1.4.11",
|
|
36977
|
+
el,
|
|
36978
|
+
msgId: "rendered-nontext-contrast",
|
|
36979
|
+
params: { ratio: contrastRatio(fill, surrounding.color).toFixed(2), control: el.tag }
|
|
36980
|
+
});
|
|
36981
|
+
}
|
|
36982
|
+
return out2;
|
|
36983
|
+
}
|
|
36984
|
+
};
|
|
36985
|
+
var FOCUS_SEL = /:focus(-visible|-within)?\b/;
|
|
36986
|
+
function killsOutline(d) {
|
|
36987
|
+
const o = (d.outline ?? "").trim();
|
|
36988
|
+
if (o === "none" || /^0(px)?( |$)/.test(o) || /\bnone\b/.test(o)) return true;
|
|
36989
|
+
if ((d.outlineStyle ?? "") === "none") return true;
|
|
36990
|
+
return d.outlineWidth !== void 0 && Number.parseFloat(d.outlineWidth) === 0;
|
|
36991
|
+
}
|
|
36992
|
+
function restoresIndicator(d) {
|
|
36993
|
+
if (d.boxShadow && d.boxShadow !== "none") return true;
|
|
36994
|
+
if (d.outlineStyle && d.outlineStyle !== "none" && Number.parseFloat(d.outlineWidth ?? "1") > 0) return true;
|
|
36995
|
+
if (d.outline && !killsOutline(d)) return true;
|
|
36996
|
+
if (d.textDecorationLine && d.textDecorationLine !== "none") return true;
|
|
36997
|
+
for (const k of Object.keys(d)) {
|
|
36998
|
+
if (/^border(Top|Right|Bottom|Left)?(Color|Width|Style)?$/.test(k) && d[k] && d[k] !== "none") return true;
|
|
36999
|
+
if (/^background(Color|Image)?$/.test(k) && d[k] && d[k] !== "none" && d[k] !== "rgba(0, 0, 0, 0)") return true;
|
|
37000
|
+
}
|
|
37001
|
+
return d.filter !== void 0 || d.transform !== void 0 || d.color !== void 0;
|
|
37002
|
+
}
|
|
37003
|
+
var renderedFocusNotVisible = {
|
|
37004
|
+
id: "rendered-focus-not-visible",
|
|
37005
|
+
criteria: ["2.4.7"],
|
|
37006
|
+
severity: "majeur",
|
|
37007
|
+
scope: "page",
|
|
37008
|
+
run(doc) {
|
|
37009
|
+
const css = doc.signals?.css;
|
|
37010
|
+
if (!css || css.unreadable > 0 || css.truncated) return [];
|
|
37011
|
+
const focusRules = css.rules.filter((r) => FOCUS_SEL.test(r.selector));
|
|
37012
|
+
if (!focusRules.length) return [];
|
|
37013
|
+
if (!focusRules.some((r) => killsOutline(r.decls))) return [];
|
|
37014
|
+
if (focusRules.some((r) => restoresIndicator(r.decls))) return [];
|
|
37015
|
+
const anchor = doc.elements.find((e) => ["a", "button", "input", "select", "textarea"].includes(e.tag));
|
|
37016
|
+
if (!anchor) return [];
|
|
37017
|
+
const killer = focusRules.find((r) => killsOutline(r.decls));
|
|
37018
|
+
return [{ criteriaId: "2.4.7", el: doc.elements[0] ?? anchor, msgId: "rendered-focus-not-visible", params: { selector: killer?.selector ?? ":focus" } }];
|
|
37019
|
+
}
|
|
37020
|
+
};
|
|
37021
|
+
var DOC_SEL = /^\s*(\*|:root|html|body)\s*$/i;
|
|
37022
|
+
function quarterTurn(transform) {
|
|
37023
|
+
const m = /rotate[ZY]?\(\s*(-?[\d.]+)deg\s*\)/i.exec(transform);
|
|
37024
|
+
if (!m) return false;
|
|
37025
|
+
const deg = Math.abs(Number.parseFloat(m[1])) % 180;
|
|
37026
|
+
return Math.abs(deg - 90) < 1;
|
|
37027
|
+
}
|
|
37028
|
+
var renderedOrientationLock = {
|
|
37029
|
+
id: "rendered-orientation-lock",
|
|
37030
|
+
criteria: ["1.3.4"],
|
|
37031
|
+
severity: "majeur",
|
|
37032
|
+
scope: "page",
|
|
37033
|
+
run(doc) {
|
|
37034
|
+
const css = doc.signals?.css;
|
|
37035
|
+
if (!css || css.unreadable > 0) return [];
|
|
37036
|
+
for (const r of css.rules) {
|
|
37037
|
+
if (!r.media || !/orientation\s*:/i.test(r.media)) continue;
|
|
37038
|
+
if (!r.selector.split(",").some((s) => DOC_SEL.test(s))) continue;
|
|
37039
|
+
const t2 = r.decls.transform ?? "";
|
|
37040
|
+
if (!quarterTurn(t2)) continue;
|
|
37041
|
+
const el = doc.elements[0];
|
|
37042
|
+
if (!el) continue;
|
|
37043
|
+
return [{ criteriaId: "1.3.4", el, msgId: "rendered-orientation-lock", params: { media: r.media, transform: t2 } }];
|
|
37044
|
+
}
|
|
37045
|
+
return [];
|
|
37046
|
+
}
|
|
37047
|
+
};
|
|
37048
|
+
var renderedRules = [
|
|
37049
|
+
renderedContrast,
|
|
37050
|
+
renderedContrastPixel,
|
|
37051
|
+
renderedLinkColourOnly,
|
|
37052
|
+
renderedNonTextContrast,
|
|
37053
|
+
renderedFocusNotVisible,
|
|
37054
|
+
renderedOrientationLock
|
|
37055
|
+
];
|
|
37056
|
+
|
|
36285
37057
|
// src/rules/timing.ts
|
|
36286
37058
|
var metaRefreshRedirect = {
|
|
36287
37059
|
id: "meta-refresh-redirect",
|
|
@@ -36329,6 +37101,7 @@ var timingRules = [metaRefreshRedirect, blinkMarquee];
|
|
|
36329
37101
|
// src/rules/registry.ts
|
|
36330
37102
|
var ALL_RULES = [
|
|
36331
37103
|
...colorsRules,
|
|
37104
|
+
...renderedRules,
|
|
36332
37105
|
...imagesRules,
|
|
36333
37106
|
...framesRules,
|
|
36334
37107
|
...scriptsAriaRules,
|
|
@@ -36390,7 +37163,7 @@ function crossToFinding(doc, ruleId, def, cf) {
|
|
|
36390
37163
|
import { resolve as absPath } from "path";
|
|
36391
37164
|
|
|
36392
37165
|
// src/graph/resolve.ts
|
|
36393
|
-
import { dirname as
|
|
37166
|
+
import { dirname as dirname6, join as join22 } from "path";
|
|
36394
37167
|
var EXT_ORDER = [".tsx", ".jsx", ".ts", ".js"];
|
|
36395
37168
|
function candidates(base) {
|
|
36396
37169
|
const out2 = [base];
|
|
@@ -36400,7 +37173,7 @@ function candidates(base) {
|
|
|
36400
37173
|
for (const e of EXT_ORDER) out2.push(stripped + e);
|
|
36401
37174
|
}
|
|
36402
37175
|
for (const e of EXT_ORDER) out2.push(base + e);
|
|
36403
|
-
for (const e of EXT_ORDER) out2.push(
|
|
37176
|
+
for (const e of EXT_ORDER) out2.push(join22(base, `index${e}`));
|
|
36404
37177
|
return out2;
|
|
36405
37178
|
}
|
|
36406
37179
|
function matchKnown(base, known) {
|
|
@@ -36412,7 +37185,7 @@ function matchKnown(base, known) {
|
|
|
36412
37185
|
}
|
|
36413
37186
|
function resolveSpecifier(fromFile, spec, known, aliases) {
|
|
36414
37187
|
if (spec.startsWith(".")) {
|
|
36415
|
-
return matchKnown(
|
|
37188
|
+
return matchKnown(join22(dirname6(toPosix(fromFile)), spec), known);
|
|
36416
37189
|
}
|
|
36417
37190
|
if (aliases?.length) {
|
|
36418
37191
|
for (const rule of aliases) {
|
|
@@ -36420,7 +37193,7 @@ function resolveSpecifier(fromFile, spec, known, aliases) {
|
|
|
36420
37193
|
if (!spec.startsWith(rule.prefix)) continue;
|
|
36421
37194
|
const rest = spec.slice(rule.prefix.length);
|
|
36422
37195
|
for (const base of rule.bases) {
|
|
36423
|
-
const hit = matchKnown(
|
|
37196
|
+
const hit = matchKnown(join22(base, rest), known);
|
|
36424
37197
|
if (hit) return hit;
|
|
36425
37198
|
}
|
|
36426
37199
|
} else if (spec === rule.prefix) {
|
|
@@ -36435,11 +37208,11 @@ function resolveSpecifier(fromFile, spec, known, aliases) {
|
|
|
36435
37208
|
}
|
|
36436
37209
|
|
|
36437
37210
|
// src/graph/tsconfig.ts
|
|
36438
|
-
import { existsSync as
|
|
36439
|
-
import { dirname as
|
|
37211
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
|
|
37212
|
+
import { dirname as dirname7, join as join23, resolve as resolve3, relative } from "path";
|
|
36440
37213
|
function readJsonish(path) {
|
|
36441
37214
|
try {
|
|
36442
|
-
const raw =
|
|
37215
|
+
const raw = readFileSync12(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/[^\n]*/g, "$1").replace(/,(\s*[}\]])/g, "$1");
|
|
36443
37216
|
return JSON.parse(raw);
|
|
36444
37217
|
} catch {
|
|
36445
37218
|
return null;
|
|
@@ -36448,9 +37221,9 @@ function readJsonish(path) {
|
|
|
36448
37221
|
function findTsconfig(startDir) {
|
|
36449
37222
|
let dir = resolve3(startDir);
|
|
36450
37223
|
for (let i2 = 0; i2 < 30; i2++) {
|
|
36451
|
-
const p =
|
|
36452
|
-
if (
|
|
36453
|
-
const parent =
|
|
37224
|
+
const p = join23(dir, "tsconfig.json");
|
|
37225
|
+
if (existsSync10(p)) return p;
|
|
37226
|
+
const parent = dirname7(dir);
|
|
36454
37227
|
if (parent === dir) break;
|
|
36455
37228
|
dir = parent;
|
|
36456
37229
|
}
|
|
@@ -36466,15 +37239,15 @@ function tsconfigChain(startDir) {
|
|
|
36466
37239
|
const p = queue.shift();
|
|
36467
37240
|
if (seen.has(p)) continue;
|
|
36468
37241
|
seen.add(p);
|
|
36469
|
-
if (!
|
|
37242
|
+
if (!existsSync10(p)) continue;
|
|
36470
37243
|
out2.push(p);
|
|
36471
37244
|
const cfg = readJsonish(p);
|
|
36472
37245
|
const exts = Array.isArray(cfg?.extends) ? cfg.extends : typeof cfg?.extends === "string" ? [cfg.extends] : [];
|
|
36473
|
-
const dir =
|
|
37246
|
+
const dir = dirname7(p);
|
|
36474
37247
|
for (const e of exts) {
|
|
36475
37248
|
if (typeof e !== "string") continue;
|
|
36476
37249
|
const cands = e.endsWith(".json") ? [resolve3(dir, e)] : [resolve3(dir, `${e}.json`), resolve3(dir, e, "tsconfig.json")];
|
|
36477
|
-
const hit = cands.find((c2) =>
|
|
37250
|
+
const hit = cands.find((c2) => existsSync10(c2));
|
|
36478
37251
|
if (hit) queue.push(hit);
|
|
36479
37252
|
}
|
|
36480
37253
|
}
|
|
@@ -36483,7 +37256,7 @@ function tsconfigChain(startDir) {
|
|
|
36483
37256
|
function readTsAliases(startDir, cwd = process.cwd()) {
|
|
36484
37257
|
const tsconfigPath = findTsconfig(startDir);
|
|
36485
37258
|
if (!tsconfigPath) return [];
|
|
36486
|
-
const root =
|
|
37259
|
+
const root = dirname7(tsconfigPath);
|
|
36487
37260
|
const cfg = readJsonish(tsconfigPath);
|
|
36488
37261
|
if (!cfg) return [];
|
|
36489
37262
|
let co = cfg.compilerOptions ?? {};
|
|
@@ -36507,7 +37280,7 @@ function readTsAliases(startDir, cwd = process.cwd()) {
|
|
|
36507
37280
|
const bases = targets.map((t2) => {
|
|
36508
37281
|
const star = t2.indexOf("*");
|
|
36509
37282
|
const tp = wildcard && star >= 0 ? t2.slice(0, star) : t2;
|
|
36510
|
-
return toPosix(relative(cwd,
|
|
37283
|
+
return toPosix(relative(cwd, join23(baseAbs, tp))) || ".";
|
|
36511
37284
|
});
|
|
36512
37285
|
out2.push({ prefix: prefix2, wildcard, bases });
|
|
36513
37286
|
}
|
|
@@ -36967,6 +37740,56 @@ var rgaa_default = {
|
|
|
36967
37740
|
en: "State the file format and size in the link text, e.g. \u201CAnnual report (PDF, 2 MB)\u201D.",
|
|
36968
37741
|
fr: "Indiquez le format et le poids du fichier dans l\u2019intitul\xE9 du lien, par exemple \xAB Rapport annuel (PDF, 2 Mo) \xBB."
|
|
36969
37742
|
}
|
|
37743
|
+
},
|
|
37744
|
+
{
|
|
37745
|
+
id: "optgroup-without-label",
|
|
37746
|
+
criterion: "11.8",
|
|
37747
|
+
wcag: ["1.3.1"],
|
|
37748
|
+
severity: "majeur",
|
|
37749
|
+
match: {
|
|
37750
|
+
tag: "optgroup",
|
|
37751
|
+
attrs: [
|
|
37752
|
+
{
|
|
37753
|
+
name: "label",
|
|
37754
|
+
op: "absent"
|
|
37755
|
+
}
|
|
37756
|
+
]
|
|
37757
|
+
},
|
|
37758
|
+
message: {
|
|
37759
|
+
en: "<optgroup> without a label attribute \u2014 the group of options is unnamed (RGAA test 11.8.2).",
|
|
37760
|
+
fr: "<optgroup> sans attribut label \u2014 le regroupement d\u2019options n\u2019est pas nomm\xE9 (test RGAA 11.8.2)."
|
|
37761
|
+
},
|
|
37762
|
+
remediation: {
|
|
37763
|
+
en: 'Add label="\u2026" naming what the options in this group have in common, e.g. <optgroup label="Europe">.',
|
|
37764
|
+
fr: 'Ajoutez label="\u2026" nommant ce que les options du groupe ont en commun, par ex. <optgroup label="Europe">.'
|
|
37765
|
+
}
|
|
37766
|
+
},
|
|
37767
|
+
{
|
|
37768
|
+
id: "dir-value-invalid",
|
|
37769
|
+
criterion: "8.10",
|
|
37770
|
+
wcag: ["1.3.2"],
|
|
37771
|
+
severity: "majeur",
|
|
37772
|
+
match: {
|
|
37773
|
+
attrs: [
|
|
37774
|
+
{
|
|
37775
|
+
name: "dir",
|
|
37776
|
+
op: "present"
|
|
37777
|
+
},
|
|
37778
|
+
{
|
|
37779
|
+
name: "dir",
|
|
37780
|
+
op: "matches",
|
|
37781
|
+
value: "^(?!(rtl|ltr|auto)$).+$"
|
|
37782
|
+
}
|
|
37783
|
+
]
|
|
37784
|
+
},
|
|
37785
|
+
message: {
|
|
37786
|
+
en: "dir attribute with a value that is not rtl, ltr or auto \u2014 the reading direction change is not declared conformantly (RGAA test 8.10.2).",
|
|
37787
|
+
fr: "Attribut dir dont la valeur n\u2019est ni rtl, ni ltr, ni auto \u2014 le changement de sens de lecture n\u2019est pas d\xE9clar\xE9 conform\xE9ment (test RGAA 8.10.2)."
|
|
37788
|
+
},
|
|
37789
|
+
remediation: {
|
|
37790
|
+
en: 'Use dir="rtl" or dir="ltr" (or dir="auto"), matching the reading direction of the text it carries.',
|
|
37791
|
+
fr: 'Utilisez dir="rtl" ou dir="ltr" (ou dir="auto"), en accord avec le sens de lecture du texte port\xE9.'
|
|
37792
|
+
}
|
|
36970
37793
|
}
|
|
36971
37794
|
],
|
|
36972
37795
|
themes: [
|
|
@@ -37697,10 +38520,16 @@ var rgaa_default = {
|
|
|
37697
38520
|
]
|
|
37698
38521
|
},
|
|
37699
38522
|
techniques: ["G18", "G136", "G148", "G174", "G145", "C29"],
|
|
37700
|
-
particularCases: [
|
|
38523
|
+
particularCases: [
|
|
38524
|
+
"Dans ces situations, les crit\xE8res sont non applicables pour ces \xE9l\xE9ments\xA0:",
|
|
38525
|
+
"- Le texte fait partie d\u2019un logo ou d\u2019un nom de marque d\u2019un organisme ou d\u2019une soci\xE9t\xE9\xA0;",
|
|
38526
|
+
"- Le texte ou l\u2019image de texte est purement d\xE9coratif\xA0;",
|
|
38527
|
+
"- Le texte fait partie d\u2019une image v\xE9hiculant une information mais le texte lui-m\xEAme n\u2019apporte aucune information essentielle\xA0;",
|
|
38528
|
+
"- Le texte ou l\u2019image de texte fait partie d\u2019un \xE9l\xE9ment d\u2019interface sur lequel aucune action n\u2019est possible (par exemple un bouton avec l\u2019attribut `disabled`)."
|
|
38529
|
+
],
|
|
37701
38530
|
wcag: ["1.4.3"],
|
|
37702
38531
|
appliesTo: {
|
|
37703
|
-
ruleIds: ["axe:color-contrast", "axe:color-contrast-enhanced", "contrast-literal"]
|
|
38532
|
+
ruleIds: ["axe:color-contrast", "axe:color-contrast-enhanced", "contrast-literal", "rendered-contrast", "rendered-contrast-pixel"]
|
|
37704
38533
|
}
|
|
37705
38534
|
},
|
|
37706
38535
|
{
|
|
@@ -37733,10 +38562,19 @@ var rgaa_default = {
|
|
|
37733
38562
|
]
|
|
37734
38563
|
},
|
|
37735
38564
|
techniques: ["G18", "G195", "G207", "G174", "G145", "G183", "F78"],
|
|
37736
|
-
particularCases: [
|
|
38565
|
+
particularCases: [
|
|
38566
|
+
"Les cas suivants sont non applicables pour ce crit\xE8re\xA0:",
|
|
38567
|
+
"- Composant d\u2019interface inactif (par exemple, un bouton avec un attribut `disabled`) sur lequel aucune action n\u2019est possible\xA0;",
|
|
38568
|
+
"- Composant d\u2019interface pour lequel l\u2019apparence est g\xE9r\xE9e par les styles natifs du navigateur sans aucune modification par l\u2019auteur (par exemple, le style au focus natif dans Chrome ou Firefox)\xA0;",
|
|
38569
|
+
"- Composant d\u2019interface pour lequel la couleur n\u2019est pas n\xE9cessaire pour identifier le composant ou son \xE9tat (par exemple, un groupe de liens faisant office de navigation dont la position dans la page, la taille et la couleur du texte permettent de comprendre qu\u2019il s\u2019agit de liens m\xEAme si la couleur du soulignement des liens avec le fond blanc n\u2019a pas un ratio de 3:1 et que le texte lui a un ratio de 4.5:1)\xA0;",
|
|
38570
|
+
"- [\xC9l\xE9ment graphique](#element-graphique) ou parties d\u2019\xE9l\xE9ment graphique non porteur d\u2019information ou ayant une alternative (description longue, informations identiques visibles dans la page)\xA0;",
|
|
38571
|
+
"- [\xC9l\xE9ment graphique](#element-graphique) ou parties d\u2019\xE9l\xE9ment graphique faisant partie d\u2019un logo ou du nom de marque d\u2019un organisme ou d\u2019une soci\xE9t\xE9\xA0;",
|
|
38572
|
+
"- [\xC9l\xE9ment graphique](#element-graphique) ou parties d\u2019\xE9l\xE9ment graphique dont la pr\xE9sentation est essentielle \xE0 l\u2019information v\xE9hicul\xE9e (par exemple, drapeaux, logotypes, photos de personnes ou de sc\xE8nes, captures d\u2019\xE9cran, diagrammes m\xE9dicaux, carte de chaleurs)\xA0;",
|
|
38573
|
+
"- [\xC9l\xE9ment graphique](#element-graphique) ou parties d\u2019\xE9l\xE9ment graphique dynamiques dont le contraste au survol / focus est suffisant."
|
|
38574
|
+
],
|
|
37737
38575
|
wcag: ["1.4.11"],
|
|
37738
38576
|
appliesTo: {
|
|
37739
|
-
ruleIds: []
|
|
38577
|
+
ruleIds: ["rendered-nontext-contrast"]
|
|
37740
38578
|
}
|
|
37741
38579
|
},
|
|
37742
38580
|
{
|
|
@@ -37774,7 +38612,13 @@ var rgaa_default = {
|
|
|
37774
38612
|
techniques: ["G58", "G69", "G78", "G158", "G159", "G173", "G8", "G166", "H96", "SM6", "SM7"],
|
|
37775
38613
|
particularCases: [
|
|
37776
38614
|
"Il existe une gestion de cas particulier lorsque\xA0:",
|
|
37777
|
-
"[
|
|
38615
|
+
"- Le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) est utilis\xE9 \xE0 des fins d\xE9coratives (c\u2019est-\xE0-dire qu\u2019il n\u2019apporte aucune information)\xA0;",
|
|
38616
|
+
"- Le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) est lui-m\xEAme une alternative \xE0 un contenu de la page (une vid\xE9o en langue des signes ou la vocalisation d\u2019un texte, par exemple)\xA0;",
|
|
38617
|
+
"- Le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) est utilis\xE9 pour acc\xE9der \xE0 une version agrandie\xA0;",
|
|
38618
|
+
"- Le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) est utilis\xE9 comme un [CAPTCHA](#captcha)\xA0;",
|
|
38619
|
+
"- Le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) fait partie d\u2019un test qui deviendrait inutile si la [transcription textuelle](#transcription-textuelle-media-temporel), les [sous-titres synchronis\xE9s](#sous-titres-synchronises-objet-multimedia) ou l\u2019[audiodescription](#audiodescription-synchronisee-media-temporel) \xE9taient communiqu\xE9s\xA0;",
|
|
38620
|
+
"- Pour les services de l\u2019\xC9tat, les collectivit\xE9s territoriales et leurs \xE9tablissements\xA0: si le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) a \xE9t\xE9 publi\xE9 entre le 23 septembre 2019 et le 23 septembre 2020 sur un site internet, intranet ou extranet cr\xE9\xE9 depuis le 23 septembre 2018, il est exempt\xE9 de l\u2019obligation d\u2019accessibilit\xE9\xA0;",
|
|
38621
|
+
"- Pour les personnes de droit priv\xE9 mentionn\xE9es aux 2\xB0 \xE0 4\xB0 du I de l\u2019article 47 de la loi du 11 f\xE9vrier 2005\xA0: si le [m\xE9dia temporel](#media-temporel-type-son-video-et-synchronise) a \xE9t\xE9 publi\xE9 avant le 23 septembre 2020, il est exempt\xE9 de l\u2019obligation d\u2019accessibilit\xE9.",
|
|
37778
38622
|
"Dans ces situations, le crit\xE8re est non applicable.",
|
|
37779
38623
|
"Ce cas particulier s\u2019applique \xE9galement aux crit\xE8res 4.2, 4.3, 4.5."
|
|
37780
38624
|
],
|
|
@@ -37959,7 +38803,13 @@ var rgaa_default = {
|
|
|
37959
38803
|
]
|
|
37960
38804
|
},
|
|
37961
38805
|
techniques: ["H35", "H46"],
|
|
37962
|
-
particularCases: [
|
|
38806
|
+
particularCases: [
|
|
38807
|
+
"Il existe une gestion de cas particulier lorsque\xA0:",
|
|
38808
|
+
"- Le [m\xE9dia non temporel](#media-non-temporel) est utilis\xE9 \xE0 des fins d\xE9coratives (c\u2019est-\xE0-dire qu\u2019il n\u2019apporte aucune information)\xA0;",
|
|
38809
|
+
"- Le [m\xE9dia non temporel](#media-non-temporel) est diffus\xE9 dans un [environnement ma\xEEtris\xE9](#environnement-maitrise)\xA0;",
|
|
38810
|
+
"- Le [m\xE9dia non temporel](#media-non-temporel) est ins\xE9r\xE9 via JavaScript en v\xE9rifiant la pr\xE9sence et la version du plug-in, en remplacement d\u2019un [contenu alternatif](#contenu-alternatif) d\xE9j\xE0 pr\xE9sent.",
|
|
38811
|
+
"Dans ces situations, le crit\xE8re est non applicable."
|
|
38812
|
+
],
|
|
37963
38813
|
wcag: ["1.1.1"],
|
|
37964
38814
|
appliesTo: {
|
|
37965
38815
|
ruleIds: []
|
|
@@ -38340,14 +39190,18 @@ var rgaa_default = {
|
|
|
38340
39190
|
techniques: ["H30", "H78", "H79", "H80", "H81", "G53", "G91", "F63", "F89", "ARIA7", "ARIA8"],
|
|
38341
39191
|
technicalNote: [
|
|
38342
39192
|
"Lorsque l\u2019intitul\xE9 visible est compl\xE9t\xE9 par une autre expression dans le nom accessible\xA0:",
|
|
38343
|
-
"
|
|
39193
|
+
"- WCAG insiste sur le placement de l\u2019intitul\xE9 visible au d\xE9but du nom accessible sans toutefois r\xE9server l\u2019exclusivit\xE9 de cet emplacement\xA0;",
|
|
39194
|
+
"- WCAG consid\xE8re comme un cas d\u2019\xE9chec une correspondance non exacte de la cha\xEEne de caract\xE8res de l\u2019intitul\xE9 visible au sein du nom accessible.",
|
|
38344
39195
|
"Par exemple, si l\u2019on consid\xE8re l\u2019intitul\xE9 visible \xAB\xA0Commander maintenant\xA0\xBB compl\xE9t\xE9 dans le nom accessible par l\u2019expression \xAB\xA0produit X\xA0\xBB, on peut avoir les diff\xE9rents cas suivants\xA0:",
|
|
38345
|
-
"
|
|
39196
|
+
"- \xAB\xA0Commander maintenant produit X\xA0\xBB est valide (bonne pratique)\xA0;",
|
|
39197
|
+
"- \xAB\xA0Produit X : commander maintenant\xA0\xBB est valide\xA0;",
|
|
39198
|
+
"- \xAB\xA0Commander produit X maintenant\xA0\xBB est non valide."
|
|
38346
39199
|
],
|
|
38347
39200
|
particularCases: [
|
|
38348
39201
|
"Il existe une gestion de cas particuliers pour les tests 6.1.1, 6.1.2, 6.1.3 et 6.1.4 lorsque le lien est [ambigu pour tout le monde](#ambigu-pour-tout-le-monde). Dans cette situation, o\xF9 il n\u2019est pas possible de rendre le lien explicite dans son contexte, le crit\xE8re est non applicable.",
|
|
38349
39202
|
"Il existe une gestion de cas particuliers pour le test 6.1.5 lorsque\xA0:",
|
|
38350
|
-
"[
|
|
39203
|
+
"- La ponctuation et les lettres majuscules sont pr\xE9sentes dans le texte de l\u2019[intitul\xE9 visible](#intitule-visible)\xA0: elles peuvent \xEAtre ignor\xE9es dans le nom accessible sans porter \xE0 cons\xE9quence\xA0;",
|
|
39204
|
+
"- Le texte de l\u2019[intitul\xE9 visible](#intitule-visible) sert de symbole\xA0: le texte ne doit pas \xEAtre interpr\xE9t\xE9 litt\xE9ralement au niveau du nom accessible. Le nom doit exprimer la fonction v\xE9hicul\xE9e par le symbole (par exemple, \u201CB\u201D au niveau d\u2019un \xE9diteur de texte aura pour nom accessible \u201CMettre en gras\u201D, le signe \u201C>\u201D en fonction du contexte signifiera \u201CSuivant\u201D ou \u201CLancer la vid\xE9o\u201D). Le cas des symboles math\xE9matiques fait cependant exception (voir la note ci-dessous).",
|
|
38351
39205
|
"Note\xA0: si l\u2019\xE9tiquette visible repr\xE9sente une expression math\xE9matique, les symboles math\xE9matiques peuvent \xEAtre repris litt\xE9ralement pour servir d\u2019\xE9tiquette au nom accessible (ex.\xA0: \u201CA>B\u201D). Il est laiss\xE9 \xE0 l\u2019utilisateur le soin d\u2019op\xE9rer la correspondance entre l\u2019expression et ce qu\u2019il doit \xE9peler compte tenu de la connaissance qu\u2019il a du fonctionnement de son logiciel de saisie vocale (\u201CA plus grand que B\u201D ou \u201CA sup\xE9rieur \xE0 B\u201D)."
|
|
38352
39206
|
],
|
|
38353
39207
|
wcag: ["1.1.1", "2.4.4", "2.5.3"],
|
|
@@ -38411,7 +39265,8 @@ var rgaa_default = {
|
|
|
38411
39265
|
],
|
|
38412
39266
|
particularCases: [
|
|
38413
39267
|
"Il existe une gestion de cas particuliers pour le test 7.1.3 lorsque\xA0:",
|
|
38414
|
-
"
|
|
39268
|
+
"- La ponctuation et les lettres majuscules sont pr\xE9sentes dans le texte de l\u2019intitul\xE9 visible : elles peuvent \xEAtre ignor\xE9es dans le nom accessible sans porter \xE0 cons\xE9quence\xA0;",
|
|
39269
|
+
"- Le texte de l\u2019intitul\xE9 visible sert de symbole : le texte ne doit pas \xEAtre interpr\xE9t\xE9 litt\xE9ralement au niveau du nom accessible. Le nom doit exprimer la fonction v\xE9hicul\xE9e par le symbole (par exemple, \u201CB\u201D au niveau d\u2019un \xE9diteur de texte aura pour nom accessible \u201CMettre en gras\u201D, le signe \u201C>\u201D en fonction du contexte signifiera \u201CSuivant\u201D ou \u201CLancer la vid\xE9o\u201D). Le cas des symboles math\xE9matiques fait cependant exception (voir la note ci-dessous).",
|
|
38415
39270
|
"Note\xA0: si l\u2019\xE9tiquette visible repr\xE9sente une expression math\xE9matique, les symboles math\xE9matiques peuvent \xEAtre repris litt\xE9ralement pour servir d\u2019\xE9tiquette au nom accessible (ex.\xA0: \u201CA>B\u201D). Il est laiss\xE9 \xE0 l\u2019utilisateur le soin d\u2019op\xE9rer la correspondance entre l\u2019expression et ce qu\u2019il doit \xE9peler compte tenu de la connaissance qu\u2019il a du fonctionnement de son logiciel de saisie vocale (\u201CA plus grand que B\u201D ou \u201CA sup\xE9rieur \xE0 B\u201D)."
|
|
38416
39271
|
],
|
|
38417
39272
|
wcag: ["2.5.3", "4.1.2"],
|
|
@@ -38541,7 +39396,9 @@ var rgaa_default = {
|
|
|
38541
39396
|
techniques: ["ARIA19", "ARIA22", "ARIA23"],
|
|
38542
39397
|
technicalNote: [
|
|
38543
39398
|
"Les r\xF4les WAI-ARIA `log`, `status` et `alert` ont implicitement une valeur d\u2019attribut WAI-ARIA `aria-live` et `aria-atomic`. On pourra donc consid\xE9rer (conform\xE9ment \xE0 la sp\xE9cification WAI-ARIA 1.1) que\xA0:",
|
|
38544
|
-
"
|
|
39399
|
+
'- Un attribut WAI-ARIA `aria-live="polite"` associ\xE9 \xE0 un message de statut peut valoir pour un r\xF4le WAI-ARIA `log`\xA0;',
|
|
39400
|
+
'- Un attribut WAI-ARIA `aria-live="polite"` et un attribut WAI-ARIA `aria-atomic="true"` associ\xE9s \xE0 un message de statut peuvent valoir pour un r\xF4le WAI-ARIA `status`\xA0;',
|
|
39401
|
+
'- Un attribut WAI-ARIA `aria-live="assertive"` et un attribut WAI-ARIA `aria-atomic="true"` associ\xE9s \xE0 un message de statut peuvent valoir pour un r\xF4le WAI-ARIA `alert`.',
|
|
38545
39402
|
"C\u2019est sous r\xE9serve que la nature du message de statut satisfasse bien \xE0 la correspondance implicitement \xE9tablie. Dans le cas d\u2019un message de statut indiquant la progression d\u2019un processus et mat\xE9rialis\xE9 graphiquement par une barre de progression, un r\xF4le WAI-ARIA `progressbar` explicite est n\xE9cessaire."
|
|
38546
39403
|
],
|
|
38547
39404
|
wcag: ["4.1.3"],
|
|
@@ -38695,7 +39552,11 @@ var rgaa_default = {
|
|
|
38695
39552
|
techniques: ["H58"],
|
|
38696
39553
|
particularCases: [
|
|
38697
39554
|
"Il y a une gestion de cas particuliers sur le changement de langue pour les cas suivants\xA0:",
|
|
38698
|
-
"
|
|
39555
|
+
"- Nom propre, le crit\xE8re est non applicable\xA0;",
|
|
39556
|
+
"- Nom commun de langue \xE9trang\xE8re pr\xE9sent dans le dictionnaire officiel de la langue (voir note 1 ci-dessous) par d\xE9faut de la page web, le crit\xE8re est non applicable\xA0;",
|
|
39557
|
+
"- Le terme de langue \xE9trang\xE8re soumis, via un [champ de formulaire](#champ-de-saisie-de-formulaire) et rappel\xE9 dans la page (par exemple comme indication du terme recherch\xE9 dans le cas d\u2019un moteur de recherche), le crit\xE8re est non applicable\xA0;",
|
|
39558
|
+
"- Passage de texte dont la langue ne peut pas \xEAtre d\xE9termin\xE9e : le crit\xE8re est non applicable\xA0;",
|
|
39559
|
+
"- Terme ou passage de texte issus d\u2019une langue morte ou imaginaire pour laquelle il n\u2019existe pas d\u2019interpr\xE9tation vocale : le crit\xE8re est non applicable.",
|
|
38699
39560
|
"Note 1\xA0: le dictionnaire officiel est celui recommand\xE9 par l\u2019acad\xE9mie en charge de la langue en question. Pour la France, par exemple, le lien vers le dictionnaire officiel se trouve sur le site de l\u2019Acad\xE9mie fran\xE7aise \xE0 l\u2019adresse suivante\xA0: http://www.academie-francaise.fr/le-dictionnaire/la-9e-edition. Pour toute demande aupr\xE8s du service du dictionnaire de l\u2019Acad\xE9mie fran\xE7aise, utiliser le formulaire de contact du service du dictionnaire.",
|
|
38700
39561
|
"Note 2\xA0: pour les noms communs de langue \xE9trang\xE8re, absents dans le dictionnaire officiel de la langue par d\xE9faut de la page web, et qui sont pass\xE9s dans le langage commun (exemple\xA0: newsletter)\xA0: le crit\xE8re est applicable, uniquement lorsque l\u2019absence d\u2019indication de langue peut provoquer une incompr\xE9hension pour la restitution."
|
|
38701
39562
|
],
|
|
@@ -38768,7 +39629,7 @@ var rgaa_default = {
|
|
|
38768
39629
|
techniques: ["H56"],
|
|
38769
39630
|
wcag: ["1.3.2"],
|
|
38770
39631
|
appliesTo: {
|
|
38771
|
-
ruleIds: []
|
|
39632
|
+
ruleIds: ["pack:rgaa:dir-value-invalid"]
|
|
38772
39633
|
}
|
|
38773
39634
|
},
|
|
38774
39635
|
{
|
|
@@ -38976,7 +39837,9 @@ var rgaa_default = {
|
|
|
38976
39837
|
techniques: ["G146", "G179", "F69", "F80", "SCR34", "C12", "C13", "C14", "C17", "C28"],
|
|
38977
39838
|
particularCases: [
|
|
38978
39839
|
"Font exception \xE0 ce crit\xE8re, les contenus pour lesquels l\u2019utilisateur n\u2019a pas de possibilit\xE9 de personnalisation\xA0:",
|
|
38979
|
-
"
|
|
39840
|
+
"- Les sous-titres incrust\xE9s dans une vid\xE9o\xA0;",
|
|
39841
|
+
"- Les textes en image\xA0;",
|
|
39842
|
+
"- Le texte au sein d\u2019une balise `<canvas>`."
|
|
38980
39843
|
],
|
|
38981
39844
|
wcag: ["1.4.4"],
|
|
38982
39845
|
appliesTo: {
|
|
@@ -39006,7 +39869,7 @@ var rgaa_default = {
|
|
|
39006
39869
|
techniques: ["F24"],
|
|
39007
39870
|
wcag: ["1.4.3"],
|
|
39008
39871
|
appliesTo: {
|
|
39009
|
-
ruleIds: ["axe:color-contrast", "axe:color-contrast-enhanced", "contrast-literal"]
|
|
39872
|
+
ruleIds: ["axe:color-contrast", "axe:color-contrast-enhanced", "contrast-literal", "rendered-contrast", "rendered-contrast-pixel"]
|
|
39010
39873
|
}
|
|
39011
39874
|
},
|
|
39012
39875
|
{
|
|
@@ -39029,7 +39892,7 @@ var rgaa_default = {
|
|
|
39029
39892
|
techniques: ["G183", "F73"],
|
|
39030
39893
|
wcag: ["1.4.1"],
|
|
39031
39894
|
appliesTo: {
|
|
39032
|
-
ruleIds: []
|
|
39895
|
+
ruleIds: ["rendered-link-colour-only"]
|
|
39033
39896
|
}
|
|
39034
39897
|
},
|
|
39035
39898
|
{
|
|
@@ -39051,7 +39914,7 @@ var rgaa_default = {
|
|
|
39051
39914
|
techniques: ["G149", "G165", "G183", "G195", "F73", "F78", "SCR31", "C15"],
|
|
39052
39915
|
wcag: ["1.4.1", "2.4.7"],
|
|
39053
39916
|
appliesTo: {
|
|
39054
|
-
ruleIds: ["dyn-focus-visible"]
|
|
39917
|
+
ruleIds: ["dyn-focus-visible", "rendered-focus-not-visible"]
|
|
39055
39918
|
}
|
|
39056
39919
|
},
|
|
39057
39920
|
{
|
|
@@ -39162,7 +40025,11 @@ var rgaa_default = {
|
|
|
39162
40025
|
particularCases: [
|
|
39163
40026
|
"L'objectif de ce crit\xE8re est de garantir un d\xE9filement dans une unique direction pour une lecture facilit\xE9e selon le sens de l'\xE9criture.",
|
|
39164
40027
|
"Font exception \xE0 ce crit\xE8re, les contenus dont l'agencement requiert deux dimensions pour \xEAtre compris ou utilis\xE9s comme\xA0:",
|
|
39165
|
-
"
|
|
40028
|
+
"- Les images, les graphiques ou les vid\xE9os\xA0;",
|
|
40029
|
+
"- Les jeux (jeux de plateforme, par exemple)\xA0;",
|
|
40030
|
+
"- Les pr\xE9sentations (type diaporama, par exemple)\xA0;",
|
|
40031
|
+
"- Les tableaux de donn\xE9es\xA0;",
|
|
40032
|
+
"- Les interfaces o\xF9 il est n\xE9cessaire d'avoir un ascenseur horizontal lors de la manipulation de l'interface.",
|
|
39166
40033
|
"Note\xA0: la majorit\xE9 des navigateurs sur les syst\xE8mes d'exploitation sur mobile (Android, iOS) ne g\xE8re pas correctement la redistribution en cas de zoom. Dans ce contexte, le crit\xE8re sera consid\xE9r\xE9 comme non applicable sur ces environnements."
|
|
39167
40034
|
],
|
|
39168
40035
|
wcag: ["1.4.10"],
|
|
@@ -39191,7 +40058,9 @@ var rgaa_default = {
|
|
|
39191
40058
|
techniques: ["C8", "C21", "C35", "C36"],
|
|
39192
40059
|
particularCases: [
|
|
39193
40060
|
"Font exception \xE0 ce crit\xE8re, les contenus pour lesquels l\u2019utilisateur n\u2019a pas de possibilit\xE9 de personnalisation\xA0:",
|
|
39194
|
-
"
|
|
40061
|
+
"- Les sous-titres directement int\xE9gr\xE9s \xE0 une vid\xE9o\xA0;",
|
|
40062
|
+
"- Les images texte\xA0;",
|
|
40063
|
+
"- Le texte au sein d\u2019une balise `<canvas>`."
|
|
39195
40064
|
],
|
|
39196
40065
|
wcag: ["1.4.12"],
|
|
39197
40066
|
appliesTo: {
|
|
@@ -39344,7 +40213,8 @@ var rgaa_default = {
|
|
|
39344
40213
|
techniques: ["G82", "G131", "H44", "H65", "ARIA6", "ARIA9", "ARIA14", "ARIA16"],
|
|
39345
40214
|
particularCases: [
|
|
39346
40215
|
"Il existe une gestion de cas particuliers pour le test 11.2.5 lorsque\xA0:",
|
|
39347
|
-
"[
|
|
40216
|
+
"- La ponctuation et les lettres majuscules sont pr\xE9sentes dans le texte de l\u2019[intitul\xE9 visible](#intitule-visible) : elles peuvent \xEAtre ignor\xE9es dans le nom accessible sans porter \xE0 cons\xE9quence\xA0;",
|
|
40217
|
+
"- Le texte de l\u2019[intitul\xE9 visible](#intitule-visible) sert de symbole\xA0: le texte ne doit pas \xEAtre interpr\xE9t\xE9 litt\xE9ralement au niveau du nom accessible. Le nom doit exprimer la fonction v\xE9hicul\xE9e par le symbole (par exemple, \u201CB\u201D au niveau d\u2019un \xE9diteur de texte aura pour nom accessible \u201CMettre en gras\u201D, le signe \u201C>\u201D en fonction du contexte signifiera \u201CSuivant\u201D ou \u201CLancer la vid\xE9o\u201D). Le cas des symboles math\xE9matiques fait cependant exception (voir la note ci-dessous).",
|
|
39348
40218
|
"Note\xA0: si l\u2019\xE9tiquette visible repr\xE9sente une expression math\xE9matique, les symboles math\xE9matiques peuvent \xEAtre repris litt\xE9ralement pour servir d\u2019\xE9tiquette au nom accessible (ex.\xA0: \u201CA>B\u201D). Il est laiss\xE9 \xE0 l\u2019utilisateur le soin d\u2019op\xE9rer la correspondance entre l\u2019expression et ce qu\u2019il doit \xE9peler compte tenu de la connaissance qu\u2019il a du fonctionnement de son logiciel de saisie vocale (\u201CA plus grand que B\u201D ou \u201CA sup\xE9rieur \xE0 B\u201D).",
|
|
39349
40219
|
"Ce cas particulier s\u2019applique \xE9galement au test 11.9.2."
|
|
39350
40220
|
],
|
|
@@ -39401,7 +40271,13 @@ var rgaa_default = {
|
|
|
39401
40271
|
]
|
|
39402
40272
|
},
|
|
39403
40273
|
techniques: ["G162"],
|
|
39404
|
-
particularCases: [
|
|
40274
|
+
particularCases: [
|
|
40275
|
+
"Les tests 11.4.2 et 11.4.3 seront consid\xE9r\xE9s comme non applicables\xA0:",
|
|
40276
|
+
"- Dans le cas o\xF9 l\u2019[\xE9tiquette](#etiquette-de-champ-de-formulaire) m\xE9lange une portion de texte qui se lit de droite \xE0 gauche avec une portion de texte qui se lit de gauche \xE0 droite\xA0;",
|
|
40277
|
+
"- Dans le cas o\xF9 un formulaire contient des labels de plusieurs langues qui se liraient de droite \xE0 gauche et inversement. Par exemple, un formulaire de commande en arabe qui propose une liste de cases \xE0 cocher de produit en langue fran\xE7aise ou mixant des produits en langue arabe ou en langue fran\xE7aise\xA0;",
|
|
40278
|
+
'- Dans le cas o\xF9 les champs de type `radio` ou `checkbox` et les balises ayant un attribut WAI-ARIA `role="checkbox"`, `role="radio"` ou `role="switch"` ne sont pas visuellement pr\xE9sent\xE9s sous forme de bouton radio ou de case \xE0 cocher\xA0;',
|
|
40279
|
+
"- Dans le cas o\xF9 les champs seraient utilis\xE9s dans un contexte o\xF9 il pourrait \xEAtre l\xE9gitime, du point de vue de l\u2019exp\xE9rience utilisateur, de placer les \xE9tiquettes de mani\xE8re diff\xE9rente \xE0 celle requise dans les tests 11.4.2 et 11.4.3."
|
|
40280
|
+
],
|
|
39405
40281
|
wcag: ["3.3.2"],
|
|
39406
40282
|
appliesTo: {
|
|
39407
40283
|
ruleIds: []
|
|
@@ -39488,7 +40364,7 @@ var rgaa_default = {
|
|
|
39488
40364
|
],
|
|
39489
40365
|
wcag: ["1.3.1"],
|
|
39490
40366
|
appliesTo: {
|
|
39491
|
-
ruleIds: []
|
|
40367
|
+
ruleIds: ["pack:rgaa:optgroup-without-label"]
|
|
39492
40368
|
}
|
|
39493
40369
|
},
|
|
39494
40370
|
{
|
|
@@ -39595,11 +40471,14 @@ var rgaa_default = {
|
|
|
39595
40471
|
],
|
|
39596
40472
|
technicalNote: [
|
|
39597
40473
|
"Dans un long formulaire dont la majorit\xE9 des champs sont obligatoires, on pourrait constater que ce sont les quelques champs rest\xE9s facultatifs qui sont explicitement signal\xE9s comme tels. Dans ce cas, il faudrait s\u2019assurer que\xA0:",
|
|
39598
|
-
"
|
|
40474
|
+
"- Un message pr\xE9cise visuellement en haut de formulaire que \u201Ctous les champs sont obligatoires sauf ceux indiqu\xE9s comme \xE9tant facultatifs\u201D\xA0;",
|
|
40475
|
+
"- Une mention \u201Cfacultatif\u201D est pr\xE9sente visuellement dans le libell\xE9 des champs facultatifs ou dans la l\xE9gende d\u2019un groupe de champs facultatifs\xA0;",
|
|
40476
|
+
'- Un attribut `required` ou `aria-required="true"` reste associ\xE9 \xE0 chaque champ qui n\u2019est pas concern\xE9 par ce caract\xE8re facultatif.'
|
|
39599
40477
|
],
|
|
39600
40478
|
particularCases: [
|
|
39601
40479
|
"Le test 11.10.1 et le test 11.10.2 seront consid\xE9r\xE9s comme non applicables lorsque le formulaire comporte un seul [champ de formulaire](#champ-de-saisie-de-formulaire) ou qu\u2019il indique les champs optionnels de mani\xE8re\xA0:",
|
|
39602
|
-
"
|
|
40480
|
+
"- Visible\xA0;",
|
|
40481
|
+
"- Dans la balise `<label>` ou dans la [l\xE9gende](#legende) associ\xE9e au champ.",
|
|
39603
40482
|
"Dans le cas o\xF9 l\u2019ensemble des champs d\u2019un formulaire sont obligatoires, les tests 11.10.1 et 11.10.2 restent applicables."
|
|
39604
40483
|
],
|
|
39605
40484
|
wcag: ["3.3.1", "3.3.2"],
|
|
@@ -39729,7 +40608,13 @@ var rgaa_default = {
|
|
|
39729
40608
|
]
|
|
39730
40609
|
},
|
|
39731
40610
|
techniques: ["G61", "F66"],
|
|
39732
|
-
particularCases: [
|
|
40611
|
+
particularCases: [
|
|
40612
|
+
"Il existe une gestion de cas particuliers lorsque\xA0:",
|
|
40613
|
+
"- La page est la page d\u2019accueil\xA0;",
|
|
40614
|
+
"- Le site web est constitu\xE9 d\u2019une seule page\xA0;",
|
|
40615
|
+
"- Le changement fait suite \xE0 une modification initi\xE9e par l\u2019utilisateur.",
|
|
40616
|
+
"Dans ces situations, le crit\xE8re est non applicable."
|
|
40617
|
+
],
|
|
39733
40618
|
wcag: ["3.2.3"],
|
|
39734
40619
|
appliesTo: {
|
|
39735
40620
|
ruleIds: []
|
|
@@ -40032,7 +40917,11 @@ var rgaa_default = {
|
|
|
40032
40917
|
]
|
|
40033
40918
|
},
|
|
40034
40919
|
techniques: ["F15", "G10", "G135"],
|
|
40035
|
-
particularCases: [
|
|
40920
|
+
particularCases: [
|
|
40921
|
+
"Il existe une gestion de cas particuliers\xA0:",
|
|
40922
|
+
"- Pour les personnes de droit priv\xE9 mentionn\xE9es aux 2\xB0 \xE0 4\xB0 du I de l\u2019article 47 de la loi du 11 f\xE9vrier 2005\xA0: si les fichiers bureautiques (ex\xA0: PDF, documents Microsoft ou LibreOffice, etc.) ont \xE9t\xE9 publi\xE9s avant le 23 septembre 2018 (sauf si ce sont des documents n\xE9cessaires pour accomplir une d\xE9marche administrative relevant des t\xE2ches effectu\xE9es par l\u2019organisme concern\xE9), ils sont exempt\xE9s de l\u2019obligation d\u2019accessibilit\xE9.",
|
|
40923
|
+
"Dans cette situation, le crit\xE8re est non applicable."
|
|
40924
|
+
],
|
|
40036
40925
|
wcag: ["1.1.1", "1.3.1", "1.3.2", "2.4.1", "2.4.3", "3.1.1", "4.1.2"],
|
|
40037
40926
|
appliesTo: {
|
|
40038
40927
|
ruleIds: []
|
|
@@ -40191,7 +41080,7 @@ var rgaa_default = {
|
|
|
40191
41080
|
],
|
|
40192
41081
|
wcag: ["1.3.4"],
|
|
40193
41082
|
appliesTo: {
|
|
40194
|
-
ruleIds: []
|
|
41083
|
+
ruleIds: ["rendered-orientation-lock"]
|
|
40195
41084
|
}
|
|
40196
41085
|
},
|
|
40197
41086
|
{
|
|
@@ -40212,7 +41101,11 @@ var rgaa_default = {
|
|
|
40212
41101
|
]
|
|
40213
41102
|
},
|
|
40214
41103
|
techniques: ["G215", "G216"],
|
|
40215
|
-
particularCases: [
|
|
41104
|
+
particularCases: [
|
|
41105
|
+
"Il existe une gestion de cas particuliers dans deux types de situation\xA0:",
|
|
41106
|
+
"- Le crit\xE8re ne s\u2019applique qu\u2019\xE0 des fonctionnalit\xE9s mises en place par l\u2019auteur du site. Il ne concerne donc pas les gestes requis par l\u2019agent utilisateur ou le syst\xE8me d\u2019exploitation\xA0;",
|
|
41107
|
+
"- Le crit\xE8re ne s\u2019applique pas aux fonctionnalit\xE9s dont la r\xE9alisation d\u2019un geste complexe est essentielle (ex\xE9cuter le trac\xE9 d\u2019une signature, par exemple)."
|
|
41108
|
+
],
|
|
40216
41109
|
wcag: ["2.5.1"],
|
|
40217
41110
|
appliesTo: {
|
|
40218
41111
|
ruleIds: []
|
|
@@ -40238,7 +41131,8 @@ var rgaa_default = {
|
|
|
40238
41131
|
techniques: [],
|
|
40239
41132
|
technicalNote: [
|
|
40240
41133
|
"Deux exemples de m\xE9canisme mis en place pour annuler ou abandonner une action d\xE9clench\xE9e au moyen d\u2019un dispositif de pointage sur un point unique de l\u2019\xE9cran\xA0:",
|
|
40241
|
-
"
|
|
41134
|
+
"- Une fen\xEAtre modale permettant d\u2019annuler l\u2019action apr\xE8s son ach\xE8vement\xA0;",
|
|
41135
|
+
"- Pour une fonction de glisser/d\xE9poser, le fait d\u2019abandonner l\u2019action si l\u2019utilisateur rel\xE2che l\u2019\xE9l\xE9ment en dehors de la zone cible."
|
|
40242
41136
|
],
|
|
40243
41137
|
particularCases: [
|
|
40244
41138
|
"Il existe une gestion de cas particulier lorsque la fonctionnalit\xE9 n\xE9cessite que le comportement attendu soit r\xE9alis\xE9 lors d\u2019un \xE9v\xE9nement descendant, par exemple, un \xE9mulateur de clavier dont les touches doivent s\u2019activer \xE0 la pression comme sur un clavier physique. Dans ces situations, le crit\xE8re est non applicable."
|
|
@@ -40269,7 +41163,11 @@ var rgaa_default = {
|
|
|
40269
41163
|
]
|
|
40270
41164
|
},
|
|
40271
41165
|
techniques: [],
|
|
40272
|
-
particularCases: [
|
|
41166
|
+
particularCases: [
|
|
41167
|
+
"Il existe une gestion de cas particulier lorsque\xA0:",
|
|
41168
|
+
"- Le mouvement est essentiel \xE0 l\u2019accomplissement de la fonctionnalit\xE9 (ex. podom\xE8tre)\xA0;",
|
|
41169
|
+
"- La d\xE9tection du mouvement est utilis\xE9e pour contr\xF4ler une fonctionnalit\xE9 au travers d\u2019une interface compatible avec l\u2019accessibilit\xE9."
|
|
41170
|
+
],
|
|
40273
41171
|
wcag: ["2.5.4"],
|
|
40274
41172
|
appliesTo: {
|
|
40275
41173
|
ruleIds: []
|
|
@@ -41210,6 +42108,9 @@ function loadPack(key) {
|
|
|
41210
42108
|
function getPack(key) {
|
|
41211
42109
|
return registry.get(key)?.pack;
|
|
41212
42110
|
}
|
|
42111
|
+
function packGlossary(key) {
|
|
42112
|
+
return registry.get(key)?.glossary;
|
|
42113
|
+
}
|
|
41213
42114
|
function listStandards() {
|
|
41214
42115
|
return [CORE_KEY, ...registry.keys()];
|
|
41215
42116
|
}
|
|
@@ -41306,7 +42207,7 @@ function runPackRules(doc, pack) {
|
|
|
41306
42207
|
}
|
|
41307
42208
|
|
|
41308
42209
|
// src/graph/build.ts
|
|
41309
|
-
import { dirname as
|
|
42210
|
+
import { dirname as dirname8 } from "path";
|
|
41310
42211
|
|
|
41311
42212
|
// src/graph/imports.ts
|
|
41312
42213
|
var NAME_PROPS2 = /* @__PURE__ */ new Set(["aria-label", "aria-labelledby", "title", "label", "alt"]);
|
|
@@ -41604,14 +42505,14 @@ function buildGraphAndDocs(files, opts = {}) {
|
|
|
41604
42505
|
}
|
|
41605
42506
|
nodes.push(extractGraphNode(ast, doc, file, { sfc }));
|
|
41606
42507
|
}
|
|
41607
|
-
const startDir = files[0] ?
|
|
42508
|
+
const startDir = files[0] ? dirname8(files[0]) : process.cwd();
|
|
41608
42509
|
return { graph: buildGraph2(nodes, readTsAliases(startDir), startDir), docs };
|
|
41609
42510
|
}
|
|
41610
42511
|
|
|
41611
42512
|
// src/discover.ts
|
|
41612
|
-
import { existsSync as
|
|
42513
|
+
import { existsSync as existsSync11 } from "fs";
|
|
41613
42514
|
import { execFileSync } from "child_process";
|
|
41614
|
-
import { join as
|
|
42515
|
+
import { join as join24, relative as relative2 } from "path";
|
|
41615
42516
|
function git(args2, maxBuffer) {
|
|
41616
42517
|
try {
|
|
41617
42518
|
return execFileSync("git", args2, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], ...maxBuffer ? { maxBuffer } : {} });
|
|
@@ -41636,7 +42537,7 @@ function gitChangedFiles(ref) {
|
|
|
41636
42537
|
add2(git(["diff", "--name-only", "--diff-filter=d", "--cached", base]));
|
|
41637
42538
|
add2(git(["ls-files", "--others", "--exclude-standard"]));
|
|
41638
42539
|
const cwd = process.cwd();
|
|
41639
|
-
return [...out2].map((p) => relative2(cwd,
|
|
42540
|
+
return [...out2].map((p) => relative2(cwd, join24(repoRoot2, p)));
|
|
41640
42541
|
}
|
|
41641
42542
|
function gitStagedFiles() {
|
|
41642
42543
|
const top = git(["rev-parse", "--show-toplevel"]);
|
|
@@ -41645,7 +42546,7 @@ function gitStagedFiles() {
|
|
|
41645
42546
|
if (out2 === null) return null;
|
|
41646
42547
|
const repoRoot2 = top.trim();
|
|
41647
42548
|
const cwd = process.cwd();
|
|
41648
|
-
return out2.split("\n").map((s) => s.trim()).filter(Boolean).map((p) => relative2(cwd,
|
|
42549
|
+
return out2.split("\n").map((s) => s.trim()).filter(Boolean).map((p) => relative2(cwd, join24(repoRoot2, p)));
|
|
41649
42550
|
}
|
|
41650
42551
|
function stagedContent(file) {
|
|
41651
42552
|
return git(["show", `:./${toPosix(file)}`], 32 * 1024 * 1024);
|
|
@@ -41730,7 +42631,7 @@ function discover(inputs, opts = {}) {
|
|
|
41730
42631
|
} else {
|
|
41731
42632
|
const filter = makeFilter(opts);
|
|
41732
42633
|
const inScope = inScopeMatcher(inputs);
|
|
41733
|
-
files = changed.filter((f) =>
|
|
42634
|
+
files = changed.filter((f) => existsSync11(f) && filter(f) && (!inScope || inScope(f)));
|
|
41734
42635
|
}
|
|
41735
42636
|
} else {
|
|
41736
42637
|
files = expandInputs(inputs, opts);
|
|
@@ -41956,7 +42857,9 @@ function runAudit(opts) {
|
|
|
41956
42857
|
}
|
|
41957
42858
|
seen.add(h);
|
|
41958
42859
|
}
|
|
41959
|
-
|
|
42860
|
+
const doc = reused ?? parseSource(content, file, { forceJsx: opts.forceJsx });
|
|
42861
|
+
attachSignals(doc);
|
|
42862
|
+
foldDoc(acc, doc, graph);
|
|
41960
42863
|
}
|
|
41961
42864
|
const canonicalFiles = acc.fileCount;
|
|
41962
42865
|
if (opts.inputs.includes("-") && opts.stdin !== void 0 && !(opts.maxFiles && opts.maxFiles > 0 && acc.fileCount >= opts.maxFiles)) {
|
|
@@ -41974,12 +42877,12 @@ function runAudit(opts) {
|
|
|
41974
42877
|
}
|
|
41975
42878
|
|
|
41976
42879
|
// src/report.ts
|
|
41977
|
-
import { mkdirSync as
|
|
41978
|
-
import { join as
|
|
42880
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
42881
|
+
import { join as join26 } from "path";
|
|
41979
42882
|
|
|
41980
42883
|
// src/prd.ts
|
|
41981
|
-
import { mkdirSync as
|
|
41982
|
-
import { join as
|
|
42884
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
42885
|
+
import { join as join25 } from "path";
|
|
41983
42886
|
|
|
41984
42887
|
// src/standards/pack.ts
|
|
41985
42888
|
function localize(pack, s, lang) {
|
|
@@ -42005,6 +42908,9 @@ function title(pack, c2, lang) {
|
|
|
42005
42908
|
function titlePlain(pack, c2, lang) {
|
|
42006
42909
|
return localize(pack, c2.titlePlain, lang);
|
|
42007
42910
|
}
|
|
42911
|
+
function resolveGlossary(packKey, anchor) {
|
|
42912
|
+
return packGlossary(packKey)?.[anchor];
|
|
42913
|
+
}
|
|
42008
42914
|
function neutralizeCaptureGroups(pattern) {
|
|
42009
42915
|
let out2 = "";
|
|
42010
42916
|
for (let i2 = 0; i2 < pattern.length; i2++) {
|
|
@@ -42104,11 +43010,29 @@ function derivePackResults(audit2, packKey) {
|
|
|
42104
43010
|
};
|
|
42105
43011
|
const enabledSecondary = (pack.secondaryMappings ?? []).filter((m) => m.enabled === true);
|
|
42106
43012
|
const secondarySources = enabledSecondary.length ? [...audit2.criteria.flatMap((c2) => c2.findings), ...myPackFindings] : [];
|
|
43013
|
+
const adjudicated = audit2.packAdjudication?.standard === packKey ? new Map(audit2.packAdjudication.criteria.map((c2) => [c2.id, c2])) : void 0;
|
|
42107
43014
|
return pack.criteria.map((pc) => {
|
|
43015
|
+
const decided = adjudicated?.get(pc.id);
|
|
43016
|
+
if (decided) {
|
|
43017
|
+
return {
|
|
43018
|
+
id: pc.id,
|
|
43019
|
+
theme: pc.theme,
|
|
43020
|
+
status: decided.status,
|
|
43021
|
+
findings: decided.findings,
|
|
43022
|
+
scs: pc.wcag,
|
|
43023
|
+
...decided.justification ? { justification: decided.justification } : {},
|
|
43024
|
+
decidedBy: "agent"
|
|
43025
|
+
};
|
|
43026
|
+
}
|
|
42108
43027
|
const base = deriveBase(pc);
|
|
42109
43028
|
return enabledSecondary.length ? applySecondaryMappings(base, pc, enabledSecondary, secondarySources, pack.defaultLocale) : base;
|
|
42110
43029
|
});
|
|
42111
43030
|
}
|
|
43031
|
+
function findingsForStandard(audit2, standard) {
|
|
43032
|
+
if (standard === CORE_KEY) return audit2.findings;
|
|
43033
|
+
const mine = (audit2.packFindings ?? []).filter((f) => f.ruleId.startsWith(`pack:${standard}:`));
|
|
43034
|
+
return mine.length ? [...audit2.findings, ...mine] : audit2.findings;
|
|
43035
|
+
}
|
|
42112
43036
|
|
|
42113
43037
|
// src/standards/vocabulary.ts
|
|
42114
43038
|
var DEFAULT = {
|
|
@@ -44948,6 +45872,9 @@ var L2 = {
|
|
|
44948
45872
|
acWhen: "un utilisateur de technologie d'assistance y acc\xE8de",
|
|
44949
45873
|
givenElements: (sel) => `les \xE9l\xE9ments ${sel} concern\xE9s`,
|
|
44950
45874
|
techniques: "Techniques WCAG",
|
|
45875
|
+
toRuleOn: "Crit\xE8res \xE0 trancher",
|
|
45876
|
+
toRuleOnNote: "Le moteur ne peut pas les d\xE9cider : ils rel\xE8vent du jugement ou du rendu. Ce ne sont PAS des non-conformit\xE9s \u2014 ils sont ind\xE9cid\xE9s. G\xE9n\xE9rez la worklist (`verify --manual --standard <pack>`), qui porte l'\xE9nonc\xE9 complet de chaque test.",
|
|
45877
|
+
tests: "tests",
|
|
44951
45878
|
docNote: "Document d'exigences (PRD) g\xE9n\xE9r\xE9 depuis l'audit statique : une \xE9pop\xE9e par th\xE8me, une user story par crit\xE8re, des crit\xE8res d'acceptation ancr\xE9s sur les intitul\xE9s WCAG. Adjugez les crit\xE8res \xAB \xE0 \xE9valuer \xBB avec `verify --manual` (agent IA, gat\xE9), le rendu via `scan`."
|
|
44952
45879
|
},
|
|
44953
45880
|
en: {
|
|
@@ -44978,6 +45905,9 @@ var L2 = {
|
|
|
44978
45905
|
acWhen: "a user of assistive technology reaches them",
|
|
44979
45906
|
givenElements: (sel) => `the affected ${sel} elements`,
|
|
44980
45907
|
techniques: "WCAG techniques",
|
|
45908
|
+
toRuleOn: "Criteria to rule on",
|
|
45909
|
+
toRuleOnNote: "The engine cannot decide these: they are judgment or rendering calls. They are NOT non-conformities \u2014 they are undecided. Generate the worklist (`verify --manual --standard <pack>`), which carries the full wording of every test.",
|
|
45910
|
+
tests: "tests",
|
|
44981
45911
|
docNote: "Product-requirements document generated from the static audit: one epic per theme, one user story per criterion, acceptance criteria anchored to the WCAG success-criterion text. Adjudicate the \u201Cto assess\u201D criteria with `verify --manual` (AI agent, gated), rendering via `scan`."
|
|
44982
45912
|
}
|
|
44983
45913
|
};
|
|
@@ -45100,6 +46030,7 @@ function renderBacklog(r, lang = "en", standard = "wcag") {
|
|
|
45100
46030
|
const out2 = header(r, lang, s.title(standardLabel(standard)), void 0, ratePct);
|
|
45101
46031
|
if (!units.length) {
|
|
45102
46032
|
out2.push(s.none, "");
|
|
46033
|
+
out2.push(...toRuleOnSection(r, standard, lang));
|
|
45103
46034
|
return out2.join("\n");
|
|
45104
46035
|
}
|
|
45105
46036
|
for (const sev of SEV_ORDER2) {
|
|
@@ -45153,6 +46084,22 @@ function acceptanceCriteria(unit, standard, lang, opts = {}) {
|
|
|
45153
46084
|
return `${prefix2}**${s.given}** ${s.givenElements(hints)} \xB7 **${s.when}** ${s.acWhen} \xB7 **${s.then}** \xAB ${req} \xBB (WCAG ${sc}).`;
|
|
45154
46085
|
});
|
|
45155
46086
|
}
|
|
46087
|
+
function toRuleOnSection(r, standard, lang) {
|
|
46088
|
+
if (isCore(standard)) return [];
|
|
46089
|
+
const s = L2[lang];
|
|
46090
|
+
const pack = loadPack(standard);
|
|
46091
|
+
const manual = derivePackResults(r, standard).filter((pc) => pc.status === "manual");
|
|
46092
|
+
if (!manual.length) return [];
|
|
46093
|
+
const out2 = [`## ${s.toRuleOn} (${manual.length})`, "", `> ${s.toRuleOnNote}`, ""];
|
|
46094
|
+
for (const pc of manual) {
|
|
46095
|
+
const crit = pack.criteria.find((c2) => c2.id === pc.id);
|
|
46096
|
+
const title2 = crit ? titlePlain(pack, crit, lang) : pc.id;
|
|
46097
|
+
const tests = Object.keys(crit?.tests ?? {});
|
|
46098
|
+
out2.push(`- [ ] **${pack.name} ${pc.id}** \u2014 ${title2}${tests.length ? ` \xB7 ${s.tests}: ${tests.map((k) => `\`${pc.id}.${k}\``).join(" ")}` : ""}`);
|
|
46099
|
+
}
|
|
46100
|
+
out2.push("");
|
|
46101
|
+
return out2;
|
|
46102
|
+
}
|
|
45156
46103
|
function renderPrdDoc(r, lang = "en", standard = "wcag") {
|
|
45157
46104
|
const s = L2[lang];
|
|
45158
46105
|
const units = prdUnits(r, standard, lang);
|
|
@@ -45162,6 +46109,7 @@ function renderPrdDoc(r, lang = "en", standard = "wcag") {
|
|
|
45162
46109
|
out2.push(s.none, "");
|
|
45163
46110
|
return out2.join("\n");
|
|
45164
46111
|
}
|
|
46112
|
+
out2.push(...toRuleOnSection(r, standard, lang));
|
|
45165
46113
|
for (const epic of epicsOf(units, standard, lang)) {
|
|
45166
46114
|
out2.push(`## ${s.epic} \u2014 ${epic.title}`, "");
|
|
45167
46115
|
for (const u of epic.units) {
|
|
@@ -45183,10 +46131,10 @@ function renderPrdDoc(r, lang = "en", standard = "wcag") {
|
|
|
45183
46131
|
return out2.join("\n");
|
|
45184
46132
|
}
|
|
45185
46133
|
function writePrd(r, opts) {
|
|
45186
|
-
|
|
46134
|
+
mkdirSync5(opts.out, { recursive: true });
|
|
45187
46135
|
if (opts.format === "doc") {
|
|
45188
|
-
const p2 =
|
|
45189
|
-
|
|
46136
|
+
const p2 = join25(opts.out, `prd-doc-${r.date}.md`);
|
|
46137
|
+
writeFileSync6(p2, renderPrdDoc(r, opts.lang, opts.standard));
|
|
45190
46138
|
return [p2];
|
|
45191
46139
|
}
|
|
45192
46140
|
const remediation = opts.format === "remediation";
|
|
@@ -45195,21 +46143,207 @@ function writePrd(r, opts) {
|
|
|
45195
46143
|
const files = remediation ? renderPerCriterion(r, opts.lang, opts.standard) : renderAuditorPerCriterion(r, opts.lang, opts.standard, { technical });
|
|
45196
46144
|
const paths = [];
|
|
45197
46145
|
for (const f of files) {
|
|
45198
|
-
const p2 =
|
|
45199
|
-
|
|
46146
|
+
const p2 = join25(opts.out, f.name);
|
|
46147
|
+
writeFileSync6(p2, f.content);
|
|
45200
46148
|
paths.push(p2);
|
|
45201
46149
|
}
|
|
45202
46150
|
return paths;
|
|
45203
46151
|
}
|
|
45204
|
-
const p =
|
|
45205
|
-
|
|
46152
|
+
const p = join25(opts.out, `prd-${r.date}.md`);
|
|
46153
|
+
writeFileSync6(p, remediation ? renderBacklog(r, opts.lang, opts.standard) : renderAuditorBacklog(r, opts.lang, opts.standard, { technical }));
|
|
45206
46154
|
return [p];
|
|
45207
46155
|
}
|
|
45208
46156
|
|
|
46157
|
+
// src/pages.ts
|
|
46158
|
+
function pageScopesFrom(snapshots) {
|
|
46159
|
+
return snapshots.map((s) => ({
|
|
46160
|
+
id: s.meta.id,
|
|
46161
|
+
name: s.meta.name,
|
|
46162
|
+
url: s.meta.url,
|
|
46163
|
+
...s.meta.auth !== void 0 ? { auth: s.meta.auth } : {},
|
|
46164
|
+
...s.meta.route ? { route: s.meta.route } : {},
|
|
46165
|
+
...s.meta.sources ? { sources: s.meta.sources } : {},
|
|
46166
|
+
...s.meta.notes ? { notes: s.meta.notes } : {},
|
|
46167
|
+
basis: "snapshot"
|
|
46168
|
+
}));
|
|
46169
|
+
}
|
|
46170
|
+
function pageScopesFromSample(sample) {
|
|
46171
|
+
return (sample?.pages ?? []).map((p) => ({
|
|
46172
|
+
id: p.id,
|
|
46173
|
+
name: p.name,
|
|
46174
|
+
url: p.url,
|
|
46175
|
+
...p.auth !== void 0 ? { auth: p.auth } : {},
|
|
46176
|
+
...p.notes ? { notes: p.notes } : {},
|
|
46177
|
+
basis: "attributed"
|
|
46178
|
+
}));
|
|
46179
|
+
}
|
|
46180
|
+
function pagesOf(result) {
|
|
46181
|
+
const fromScope = result.scope.pages ?? [];
|
|
46182
|
+
const ids = new Set(fromScope.map((p) => p.id));
|
|
46183
|
+
const urls = new Set(fromScope.map((p) => p.url));
|
|
46184
|
+
const extra = pageScopesFromSample(result.scope.sample).filter((p) => !ids.has(p.id) && !urls.has(p.url));
|
|
46185
|
+
return [...fromScope, ...extra];
|
|
46186
|
+
}
|
|
46187
|
+
function pathMatch2(a, b) {
|
|
46188
|
+
const x = a.split("\\").join("/");
|
|
46189
|
+
const y = b.split("\\").join("/");
|
|
46190
|
+
return x === y || x.endsWith(`/${y}`) || y.endsWith(`/${x}`);
|
|
46191
|
+
}
|
|
46192
|
+
function attributePages(result, pages) {
|
|
46193
|
+
if (!pages.length) return;
|
|
46194
|
+
const byName = new Map(pages.map((p) => [p.name.toLowerCase(), p.id]));
|
|
46195
|
+
const byUrl = new Map(pages.map((p) => [p.url, p.id]));
|
|
46196
|
+
for (const f of [...result.findings, ...result.packFindings ?? []]) {
|
|
46197
|
+
if (f.page) continue;
|
|
46198
|
+
if (isUrlPath(f.file)) {
|
|
46199
|
+
const hit = byUrl.get(f.file);
|
|
46200
|
+
if (hit) f.page = hit;
|
|
46201
|
+
continue;
|
|
46202
|
+
}
|
|
46203
|
+
const sampleName = f.sample?.page?.toLowerCase();
|
|
46204
|
+
if (sampleName && byName.has(sampleName)) {
|
|
46205
|
+
f.page = byName.get(sampleName);
|
|
46206
|
+
continue;
|
|
46207
|
+
}
|
|
46208
|
+
const src = f.origin?.sourceFile ?? f.file;
|
|
46209
|
+
for (const p of pages) {
|
|
46210
|
+
if (p.sources?.some((s) => pathMatch2(src, s))) {
|
|
46211
|
+
f.page = p.id;
|
|
46212
|
+
break;
|
|
46213
|
+
}
|
|
46214
|
+
}
|
|
46215
|
+
}
|
|
46216
|
+
}
|
|
46217
|
+
function unattributedFindings(result) {
|
|
46218
|
+
return result.findings.filter((f) => !f.page);
|
|
46219
|
+
}
|
|
46220
|
+
function pageStatus(c2, pageFindings, basis) {
|
|
46221
|
+
if (pageFindings.some((f) => !f.advisory)) return "NC";
|
|
46222
|
+
if (c2.status === "manual") return "manual";
|
|
46223
|
+
if (c2.status === "NA") return "NA";
|
|
46224
|
+
return basis === "snapshot" ? "C" : "manual";
|
|
46225
|
+
}
|
|
46226
|
+
function pct(criteria) {
|
|
46227
|
+
const c2 = criteria.filter((x) => x.status === "C").length;
|
|
46228
|
+
const nc = criteria.filter((x) => x.status === "NC").length;
|
|
46229
|
+
return c2 + nc === 0 ? 100 : Math.round(c2 / (c2 + nc) * 100);
|
|
46230
|
+
}
|
|
46231
|
+
function derivePages(result, pages) {
|
|
46232
|
+
if (!pages.length) return [];
|
|
46233
|
+
const out2 = [];
|
|
46234
|
+
for (const p of pages) {
|
|
46235
|
+
const own = result.findings.filter((f) => f.page === p.id);
|
|
46236
|
+
const criteria = result.criteria.map((c2) => {
|
|
46237
|
+
const pf = own.filter((f) => f.criteriaId === c2.id);
|
|
46238
|
+
return {
|
|
46239
|
+
id: c2.id,
|
|
46240
|
+
guideline: c2.guideline,
|
|
46241
|
+
status: pageStatus(c2, pf, p.basis),
|
|
46242
|
+
findings: pf,
|
|
46243
|
+
...c2.justification ? { justification: c2.justification } : {},
|
|
46244
|
+
...c2.decidedBy ? { decidedBy: c2.decidedBy } : {}
|
|
46245
|
+
};
|
|
46246
|
+
});
|
|
46247
|
+
out2.push({
|
|
46248
|
+
id: p.id,
|
|
46249
|
+
name: p.name,
|
|
46250
|
+
url: p.url,
|
|
46251
|
+
...p.auth !== void 0 ? { auth: p.auth } : {},
|
|
46252
|
+
basis: p.basis,
|
|
46253
|
+
criteria,
|
|
46254
|
+
findings: own,
|
|
46255
|
+
conformancePct: pct(criteria)
|
|
46256
|
+
});
|
|
46257
|
+
}
|
|
46258
|
+
return out2;
|
|
46259
|
+
}
|
|
46260
|
+
var MARK = { C: "C", NC: "NC", NA: "\u2014", manual: "?" };
|
|
46261
|
+
var L3 = {
|
|
46262
|
+
fr: {
|
|
46263
|
+
title: "Grille par page",
|
|
46264
|
+
note: "Statut de chaque crit\xE8re, page par page. `C` conforme \xB7 `NC` non conforme \xB7 `\u2014` non applicable \xB7 `?` \xE0 \xE9valuer.",
|
|
46265
|
+
criterion: "Crit\xE8re",
|
|
46266
|
+
theme: "Th\xE9matique",
|
|
46267
|
+
none: "Aucune page dans le p\xE9rim\xE8tre : aucun instantan\xE9 (.ultra11y/pages) ni \xE9chantillon scann\xE9.",
|
|
46268
|
+
basisNote: "Une page marqu\xE9e \xAB source \xBB n'a pas d'instantan\xE9 : ses constats proviennent du code, donc l'absence de constat n'y vaut PAS conformit\xE9 \u2014 les crit\xE8res restent \xAB \xE0 \xE9valuer \xBB.",
|
|
46269
|
+
unattributed: (n) => `${n} constat(s) non rattach\xE9(s) \xE0 une page (code partag\xE9, fichier hors routes) \u2014 compt\xE9s dans l'audit global, jamais r\xE9partis d'office.`,
|
|
46270
|
+
rate: "Taux",
|
|
46271
|
+
snapshot: "instantan\xE9",
|
|
46272
|
+
source: "source"
|
|
46273
|
+
},
|
|
46274
|
+
en: {
|
|
46275
|
+
title: "Per-page grid",
|
|
46276
|
+
note: "Each criterion's status, page by page. `C` conforming \xB7 `NC` non-conforming \xB7 `\u2014` not applicable \xB7 `?` to assess.",
|
|
46277
|
+
criterion: "Criterion",
|
|
46278
|
+
theme: "Theme",
|
|
46279
|
+
none: "No page in scope: no snapshot (.ultra11y/pages) and no scanned sample.",
|
|
46280
|
+
basisNote: 'A page marked "source" has no snapshot: its findings come from the code, so the absence of a finding there does NOT mean conforming \u2014 those criteria stay "to assess".',
|
|
46281
|
+
unattributed: (n) => `${n} unattributed finding(s) (shared code, file outside any route) \u2014 counted in the overall audit, never spread across pages.`,
|
|
46282
|
+
rate: "Rate",
|
|
46283
|
+
snapshot: "snapshot",
|
|
46284
|
+
source: "source"
|
|
46285
|
+
}
|
|
46286
|
+
};
|
|
46287
|
+
function pageView(result, page) {
|
|
46288
|
+
return {
|
|
46289
|
+
...result,
|
|
46290
|
+
criteria: page.criteria,
|
|
46291
|
+
findings: page.findings,
|
|
46292
|
+
...result.packFindings ? { packFindings: result.packFindings.filter((f) => f.page === page.id) } : {}
|
|
46293
|
+
};
|
|
46294
|
+
}
|
|
46295
|
+
function gridOf(result, derived, standard, lang) {
|
|
46296
|
+
const status = /* @__PURE__ */ new Map();
|
|
46297
|
+
const put = (rowId, pageId, s) => {
|
|
46298
|
+
const m = status.get(rowId) ?? /* @__PURE__ */ new Map();
|
|
46299
|
+
m.set(pageId, s);
|
|
46300
|
+
status.set(rowId, m);
|
|
46301
|
+
};
|
|
46302
|
+
if (isCore(standard)) {
|
|
46303
|
+
const rows2 = [...result.criteria].sort((a, b) => compareSC(a.id, b.id)).map((c2) => ({ id: c2.id, label: `${c2.id} ${scTitle(c2.id, lang) ?? ""}`.trim(), group: c2.guideline }));
|
|
46304
|
+
for (const p of derived) for (const c2 of p.criteria) put(c2.id, p.id, c2.status);
|
|
46305
|
+
return { rows: rows2, status };
|
|
46306
|
+
}
|
|
46307
|
+
const pack = loadPack(standard);
|
|
46308
|
+
const rows = pack.criteria.map((pc) => ({ id: pc.id, label: pc.id, group: `${pc.theme}. ${themeName(pack, pc.theme, lang) ?? ""}`.trim() }));
|
|
46309
|
+
for (const p of derived) for (const pc of derivePackResults(pageView(result, p), standard)) put(pc.id, p.id, pc.status);
|
|
46310
|
+
return { rows, status };
|
|
46311
|
+
}
|
|
46312
|
+
function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
|
|
46313
|
+
const s = L3[lang];
|
|
46314
|
+
const out2 = [];
|
|
46315
|
+
out2.push(`## \u{1F4CA} ${s.title}`, "");
|
|
46316
|
+
const derived = derivePages(result, pages);
|
|
46317
|
+
if (!derived.length) {
|
|
46318
|
+
out2.push(s.none, "");
|
|
46319
|
+
return out2.join("\n");
|
|
46320
|
+
}
|
|
46321
|
+
out2.push(`> ${s.note}`, "");
|
|
46322
|
+
if (derived.some((p) => p.basis === "attributed")) out2.push(`> \u26A0\uFE0F ${s.basisNote}`, "");
|
|
46323
|
+
const head = [isCore(standard) ? s.criterion : s.criterion, ...derived.map((p) => `${p.name}${p.auth ? " \u{1F512}" : ""}`)];
|
|
46324
|
+
out2.push(`| ${head.join(" | ")} |`, `| ${head.map(() => "---").join(" | ")} |`);
|
|
46325
|
+
out2.push(`| **${s.rate}** | ${derived.map((p) => `**${p.conformancePct}%**`).join(" | ")} |`);
|
|
46326
|
+
out2.push(`| _${s.snapshot}?_ | ${derived.map((p) => p.basis === "snapshot" ? `_${s.snapshot}_` : `_${s.source}_`).join(" | ")} |`);
|
|
46327
|
+
const { rows, status } = gridOf(result, derived, standard, lang);
|
|
46328
|
+
let group = "";
|
|
46329
|
+
for (const row of rows) {
|
|
46330
|
+
if (row.group !== group) {
|
|
46331
|
+
group = row.group;
|
|
46332
|
+
out2.push(`| **${group}** | ${derived.map(() => "").join(" | ")} |`);
|
|
46333
|
+
}
|
|
46334
|
+
const cells = derived.map((p) => MARK[status.get(row.id)?.get(p.id) ?? "manual"]);
|
|
46335
|
+
out2.push(`| ${row.label} | ${cells.join(" | ")} |`);
|
|
46336
|
+
}
|
|
46337
|
+
out2.push("");
|
|
46338
|
+
const orphans = unattributedFindings(result);
|
|
46339
|
+
if (orphans.length) out2.push(`> ${s.unattributed(orphans.length)}`, "");
|
|
46340
|
+
return out2.join("\n");
|
|
46341
|
+
}
|
|
46342
|
+
|
|
45209
46343
|
// src/report.ts
|
|
45210
46344
|
var ICON3 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
|
|
45211
46345
|
var SEV_ORDER3 = ["bloquant", "majeur", "mineur"];
|
|
45212
|
-
var
|
|
46346
|
+
var L4 = {
|
|
45213
46347
|
fr: {
|
|
45214
46348
|
title: (std) => `Rapport d'audit d'accessibilit\xE9 \u2014 ${std}`,
|
|
45215
46349
|
wcagStd: "WCAG 2.2 niveau AA",
|
|
@@ -45236,6 +46370,8 @@ var L3 = {
|
|
|
45236
46370
|
naTitle: "4. Crit\xE8res non applicables (NA)",
|
|
45237
46371
|
manualTitle: "5. Crit\xE8res \xE0 adjuger (jugement / rendu) \u2014 non d\xE9cid\xE9s par le moteur statique",
|
|
45238
46372
|
manualWarn: "Adjugez-les avec `verify --manual` (l'agent d\xE9cide depuis la source, de fa\xE7on gat\xE9e) ; les crit\xE8res de rendu passent par `scan`. Aucun ne doit \xEAtre marqu\xE9 \xAB conforme \xBB sans justification enregistr\xE9e et gat\xE9e.",
|
|
46373
|
+
testsToRule: "tests \xE0 trancher",
|
|
46374
|
+
manualHowTo: "G\xE9n\xE9rez la worklist : `verify --manual --in <audit.json> --standard <pack> --out <dir>`. Chaque item y porte l'\xE9nonc\xE9 complet de ses tests, sa note technique, ses cas particuliers, sa guidance et les termes que le r\xE9f\xE9rentiel d\xE9finit.",
|
|
45239
46375
|
outOfScope: "Hors p\xE9rim\xE8tre moteur \u2014 mapp\xE9 sur des SC hors WCAG 2.2 AA ; v\xE9rification manuelle.",
|
|
45240
46376
|
scopedOut: "Les non-conformit\xE9s WCAG relev\xE9es concernent des \xE9l\xE9ments hors du p\xE9rim\xE8tre de ce crit\xE8re \u2014 \xE0 \xE9valuer s\xE9par\xE9ment.",
|
|
45241
46377
|
nothing: "Aucun.",
|
|
@@ -45285,6 +46421,8 @@ var L3 = {
|
|
|
45285
46421
|
naTitle: "4. Not-applicable criteria (NA)",
|
|
45286
46422
|
manualTitle: "5. Criteria to adjudicate (judgment / rendering) \u2014 not decided by the static engine",
|
|
45287
46423
|
manualWarn: "Adjudicate these with `verify --manual` (the agent decides from source, gated); rendering criteria go to `scan`. None may be marked \u201Cconforming\u201D without a recorded, gated justification.",
|
|
46424
|
+
testsToRule: "tests to rule on",
|
|
46425
|
+
manualHowTo: "Generate the worklist: `verify --manual --in <audit.json> --standard <pack> --out <dir>`. Each item carries the full wording of its tests, its technical note, its particular cases, its guidance and the terms the standard defines.",
|
|
45288
46426
|
outOfScope: "Out of engine scope \u2014 mapped to SCs outside WCAG 2.2 AA; manual verification.",
|
|
45289
46427
|
scopedOut: "The WCAG failures found concern elements outside this criterion's scope \u2014 assess separately.",
|
|
45290
46428
|
nothing: "None.",
|
|
@@ -45325,10 +46463,10 @@ function untestedNeedsRendering(r) {
|
|
|
45325
46463
|
function partialAuditBanner(lang, untested = NEEDS_RENDERING.map((c2) => c2.sc)) {
|
|
45326
46464
|
const set = new Set(untested);
|
|
45327
46465
|
const labels = NEEDS_RENDERING.filter((c2) => set.has(c2.sc)).map((c2) => c2.label[lang]);
|
|
45328
|
-
return
|
|
46466
|
+
return L4[lang].partialAudit(labels.join(", "));
|
|
45329
46467
|
}
|
|
45330
46468
|
function render(r, lang, opts) {
|
|
45331
|
-
const s =
|
|
46469
|
+
const s = L4[lang];
|
|
45332
46470
|
const out2 = [];
|
|
45333
46471
|
out2.push(`# ${s.title(opts.std)}`, "");
|
|
45334
46472
|
out2.push(`- **${s.date}** : ${r.date}`);
|
|
@@ -45378,6 +46516,8 @@ function render(r, lang, opts) {
|
|
|
45378
46516
|
out2.push(`## \u{1F4A1} ${s.recTitle}`, "", `> ${s.recNote}`, "");
|
|
45379
46517
|
for (const u of advisoryUnits) out2.push(...renderAuditorUnit(u, opts.standard, lang, { heading: "###" }));
|
|
45380
46518
|
}
|
|
46519
|
+
const pageScope = pagesOf(r);
|
|
46520
|
+
if (pageScope.length) out2.push(renderPageGrid(r, pageScope, opts.standard, lang));
|
|
45381
46521
|
if (r.scope.sample?.pages.length) {
|
|
45382
46522
|
out2.push(`## \u{1F4C4} ${s.perPageTitle}`, "", `> ${s.perPageNote}`, "");
|
|
45383
46523
|
if (r.scope.sample.transverse?.length) out2.push(`> ${s.transverseNote(r.scope.sample.transverse.join(", "))}`, "");
|
|
@@ -45405,11 +46545,20 @@ function render(r, lang, opts) {
|
|
|
45405
46545
|
out2.push(na.length ? na.map((x) => `- ${x.label}${x.justification ? ` \u2014 _${x.justification}_` : ""}`).join("\n") : s.nothing, "");
|
|
45406
46546
|
out2.push(`## ${s.manualTitle}`, "", `> ${s.manualWarn}`, "");
|
|
45407
46547
|
const manual = rows.filter((x) => x.status === "manual");
|
|
45408
|
-
|
|
46548
|
+
if (!manual.length) out2.push(s.nothing, "");
|
|
46549
|
+
else {
|
|
46550
|
+
const pack5 = isCore(opts.standard) ? void 0 : loadPack(opts.standard);
|
|
46551
|
+
for (const x of manual) {
|
|
46552
|
+
const tests = pack5 ? Object.keys(getCriterion(pack5, x.id)?.tests ?? {}) : [];
|
|
46553
|
+
const testRef = tests.length ? ` \u2014 ${s.testsToRule}: ${tests.map((k) => `\`${x.id}.${k}\``).join(" \xB7 ")}` : "";
|
|
46554
|
+
out2.push(`- ${x.label}${x.justification ? ` \u2014 _${x.justification}_` : ""}${testRef}`);
|
|
46555
|
+
}
|
|
46556
|
+
out2.push("", `> ${s.manualHowTo}`, "");
|
|
46557
|
+
}
|
|
45409
46558
|
return out2.join("\n");
|
|
45410
46559
|
}
|
|
45411
46560
|
function renderReport(r, lang = "en") {
|
|
45412
|
-
const s =
|
|
46561
|
+
const s = L4[lang];
|
|
45413
46562
|
const byGuideline = /* @__PURE__ */ new Map();
|
|
45414
46563
|
for (const c2 of r.criteria) {
|
|
45415
46564
|
const title2 = scTitle(c2.id, lang);
|
|
@@ -45422,7 +46571,7 @@ function renderReport(r, lang = "en") {
|
|
|
45422
46571
|
function renderPackReport(r, pack, lang = "en") {
|
|
45423
46572
|
const derived = derivePackResults(r, pack.key);
|
|
45424
46573
|
const std = `${pack.name} ${pack.baseVersion}`;
|
|
45425
|
-
const s =
|
|
46574
|
+
const s = L4[lang];
|
|
45426
46575
|
const naReason = lang === "fr" ? "Aucun crit\xE8re de succ\xE8s WCAG mapp\xE9 n'est applicable dans le p\xE9rim\xE8tre." : "No mapped WCAG success criterion is applicable in scope.";
|
|
45427
46576
|
const byTheme = /* @__PURE__ */ new Map();
|
|
45428
46577
|
for (const pr of derived) {
|
|
@@ -45441,7 +46590,7 @@ function renderPackReport(r, pack, lang = "en") {
|
|
|
45441
46590
|
const groups = pack.themes.map((t2) => ({ key: `${t2.number}.`, title: themeName(pack, t2.number, lang) ?? "", rows: byTheme.get(t2.number) ?? [] }));
|
|
45442
46591
|
return render(r, lang, {
|
|
45443
46592
|
std,
|
|
45444
|
-
groupHead:
|
|
46593
|
+
groupHead: L4[lang].byTheme,
|
|
45445
46594
|
groups,
|
|
45446
46595
|
derivedOf: std,
|
|
45447
46596
|
standard: pack.key,
|
|
@@ -45452,9 +46601,9 @@ function renderPackReport(r, pack, lang = "en") {
|
|
|
45452
46601
|
function writeReport(r, opts) {
|
|
45453
46602
|
const core = isCore(opts.standard);
|
|
45454
46603
|
const md = core ? renderReport(r, opts.lang) : renderPackReport(r, loadPack(opts.standard), opts.lang);
|
|
45455
|
-
|
|
45456
|
-
const path =
|
|
45457
|
-
|
|
46604
|
+
mkdirSync6(opts.out, { recursive: true });
|
|
46605
|
+
const path = join26(opts.out, `${core ? "wcag" : opts.standard}-${r.date}.md`);
|
|
46606
|
+
writeFileSync7(path, md);
|
|
45458
46607
|
return path;
|
|
45459
46608
|
}
|
|
45460
46609
|
|
|
@@ -45596,6 +46745,46 @@ function pushSingleIssue(units, lang, standard = "wcag", format = "audit") {
|
|
|
45596
46745
|
}
|
|
45597
46746
|
return result;
|
|
45598
46747
|
}
|
|
46748
|
+
function COMMENT_MARKER(standard) {
|
|
46749
|
+
return `<!-- ultra11y:report standard="${standard}" -->`;
|
|
46750
|
+
}
|
|
46751
|
+
function stickyBody(markdown, standard) {
|
|
46752
|
+
return `${COMMENT_MARKER(standard)}
|
|
46753
|
+
${markdown}`;
|
|
46754
|
+
}
|
|
46755
|
+
function prNumberFromEnv(env = process.env) {
|
|
46756
|
+
const explicit = Number.parseInt(env.ULTRA11Y_PR ?? "", 10);
|
|
46757
|
+
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
46758
|
+
const m = /^refs\/pull\/(\d+)\//.exec(env.GITHUB_REF ?? "");
|
|
46759
|
+
const n = m ? Number.parseInt(m[1], 10) : Number.NaN;
|
|
46760
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
46761
|
+
}
|
|
46762
|
+
function pickExistingComment(comments, marker) {
|
|
46763
|
+
return comments.find((c2) => typeof c2.body === "string" && c2.body.includes(marker));
|
|
46764
|
+
}
|
|
46765
|
+
function pushPrComment(markdown, standard = "wcag") {
|
|
46766
|
+
const pr = prNumberFromEnv();
|
|
46767
|
+
if (pr === void 0) return { ok: true, action: "skipped", reason: "not a pull-request run" };
|
|
46768
|
+
const marker = COMMENT_MARKER(standard);
|
|
46769
|
+
const body2 = stickyBody(markdown, standard);
|
|
46770
|
+
try {
|
|
46771
|
+
const repo = gh(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]).trim();
|
|
46772
|
+
let existing;
|
|
46773
|
+
try {
|
|
46774
|
+
const raw = gh(["api", `repos/${repo}/issues/${pr}/comments`, "--paginate"]);
|
|
46775
|
+
existing = pickExistingComment(JSON.parse(raw), marker);
|
|
46776
|
+
} catch {
|
|
46777
|
+
}
|
|
46778
|
+
if (existing) {
|
|
46779
|
+
gh(["api", "--method", "PATCH", `repos/${repo}/issues/comments/${existing.id}`, "-f", `body=${body2}`]);
|
|
46780
|
+
return { ok: true, action: "updated" };
|
|
46781
|
+
}
|
|
46782
|
+
gh(["api", "--method", "POST", `repos/${repo}/issues/${pr}/comments`, "-f", `body=${body2}`]);
|
|
46783
|
+
return { ok: true, action: "created" };
|
|
46784
|
+
} catch (e) {
|
|
46785
|
+
return { ok: false, action: "skipped", reason: ghErrorReason(e) };
|
|
46786
|
+
}
|
|
46787
|
+
}
|
|
45599
46788
|
|
|
45600
46789
|
// src/render.ts
|
|
45601
46790
|
var KNOWN_LIBS = {
|
|
@@ -45626,7 +46815,7 @@ function detectFrameworks(deps, has2) {
|
|
|
45626
46815
|
for (const [pkg, label] of Object.entries(KNOWN_LIBS)) if (dep(pkg)) componentLibraries.push(label);
|
|
45627
46816
|
return { frameworks, componentLibraries };
|
|
45628
46817
|
}
|
|
45629
|
-
var
|
|
46818
|
+
var L5 = {
|
|
45630
46819
|
fr: {
|
|
45631
46820
|
title: "Obtenir du HTML rendu \xE0 auditer (render)",
|
|
45632
46821
|
why: "Auditer les sources JSX d'une biblioth\xE8que de composants donne des faux n\xE9gatifs : il faut auditer le HTML r\xE9ellement produit.",
|
|
@@ -45657,7 +46846,7 @@ var L4 = {
|
|
|
45657
46846
|
}
|
|
45658
46847
|
};
|
|
45659
46848
|
function renderPlan(d, lang = "fr") {
|
|
45660
|
-
const s =
|
|
46849
|
+
const s = L5[lang];
|
|
45661
46850
|
const out2 = [`# ${s.title}`, "", `> ${s.why}`, ""];
|
|
45662
46851
|
if (d.componentLibraries.length) out2.push(`> \u{1F9E9} ${s.libNote(d.componentLibraries.join(", "))}`, "");
|
|
45663
46852
|
out2.push(`## ${s.detected}`, "");
|
|
@@ -45918,8 +47107,40 @@ function runWcag(opts) {
|
|
|
45918
47107
|
console.log(opts.json ? JSON.stringify(allSC(), null, 2) : wcagList(opts.lang));
|
|
45919
47108
|
return 0;
|
|
45920
47109
|
}
|
|
47110
|
+
var foldTerm = (s) => s.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
47111
|
+
function runGlossary(opts) {
|
|
47112
|
+
const pack = loadPack(opts.standard);
|
|
47113
|
+
const glossary = packGlossary(opts.standard) ?? {};
|
|
47114
|
+
const anchors = Object.keys(glossary);
|
|
47115
|
+
if (!anchors.length) {
|
|
47116
|
+
console.error(`ultra11y criteria: ${pack.name} ships no glossary.`);
|
|
47117
|
+
return 2;
|
|
47118
|
+
}
|
|
47119
|
+
const term = typeof opts.glossary === "string" ? opts.glossary.trim() : "";
|
|
47120
|
+
if (!term) {
|
|
47121
|
+
if (opts.json) console.log(JSON.stringify(glossary, null, 2));
|
|
47122
|
+
else for (const a of anchors.sort()) console.log(`${a} ${glossary[a]?.title ?? ""}`);
|
|
47123
|
+
return 0;
|
|
47124
|
+
}
|
|
47125
|
+
const want = foldTerm(term);
|
|
47126
|
+
const hit = anchors.find((a) => a === term) ?? anchors.find((a) => foldTerm(a) === want) ?? anchors.find((a) => foldTerm(glossary[a]?.title ?? "") === want) ?? anchors.find((a) => foldTerm(a).startsWith(want));
|
|
47127
|
+
if (!hit) {
|
|
47128
|
+
const near = anchors.filter((a) => foldTerm(a).includes(want) || foldTerm(glossary[a]?.title ?? "").includes(want)).slice(0, 8);
|
|
47129
|
+
console.error(`ultra11y criteria: no ${pack.name} glossary term matching "${term}".${near.length ? ` Did you mean: ${near.join(", ")}?` : ""}`);
|
|
47130
|
+
return 2;
|
|
47131
|
+
}
|
|
47132
|
+
const entry = glossary[hit];
|
|
47133
|
+
if (opts.json) console.log(JSON.stringify({ anchor: hit, ...entry }, null, 2));
|
|
47134
|
+
else {
|
|
47135
|
+
console.log(`${pack.name} \u2014 ${entry.title} (#${hit})`);
|
|
47136
|
+
console.log("");
|
|
47137
|
+
console.log(entry.body);
|
|
47138
|
+
}
|
|
47139
|
+
return 0;
|
|
47140
|
+
}
|
|
45921
47141
|
function runPack(opts) {
|
|
45922
47142
|
const pack = loadPack(opts.standard);
|
|
47143
|
+
if (opts.glossary !== void 0 && opts.glossary !== false) return runGlossary(opts);
|
|
45923
47144
|
if (opts.id) {
|
|
45924
47145
|
const c2 = getCriterion(pack, opts.id);
|
|
45925
47146
|
if (!c2) {
|
|
@@ -45943,6 +47164,10 @@ function runPack(opts) {
|
|
|
45943
47164
|
return 0;
|
|
45944
47165
|
}
|
|
45945
47166
|
function runCriteria(opts) {
|
|
47167
|
+
if (opts.glossary !== void 0 && opts.glossary !== false && isCore(opts.standard)) {
|
|
47168
|
+
console.error("ultra11y criteria: --glossary needs a country standard (e.g. --standard rgaa); WCAG ships no glossary here.");
|
|
47169
|
+
return 2;
|
|
47170
|
+
}
|
|
45946
47171
|
return isCore(opts.standard) ? runWcag(opts) : runPack(opts);
|
|
45947
47172
|
}
|
|
45948
47173
|
function renderCriteriaReference() {
|
|
@@ -45977,12 +47202,12 @@ function renderCriteriaReference() {
|
|
|
45977
47202
|
}
|
|
45978
47203
|
|
|
45979
47204
|
// src/check.ts
|
|
45980
|
-
import { existsSync as
|
|
45981
|
-
import { dirname as
|
|
47205
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
|
|
47206
|
+
import { dirname as dirname9, join as join28 } from "path";
|
|
45982
47207
|
|
|
45983
47208
|
// src/verify.ts
|
|
45984
|
-
import { mkdirSync as
|
|
45985
|
-
import { join as
|
|
47209
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
47210
|
+
import { join as join27 } from "path";
|
|
45986
47211
|
var VERIFY_MAX = 40;
|
|
45987
47212
|
var plain = (s) => s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
|
|
45988
47213
|
function auditorCriterionLine(standard) {
|
|
@@ -46156,16 +47381,16 @@ function applyVerdicts(items, expected) {
|
|
|
46156
47381
|
return { ok: failures.length === 0, total, refuted, unsupported, unadjudicated, invalid, missing, failures };
|
|
46157
47382
|
}
|
|
46158
47383
|
function writeWorklist(items, outDir, semantic, standard = "wcag", lang = "en") {
|
|
46159
|
-
|
|
46160
|
-
const todoPath =
|
|
46161
|
-
const mdPath =
|
|
46162
|
-
|
|
46163
|
-
|
|
47384
|
+
mkdirSync7(outDir, { recursive: true });
|
|
47385
|
+
const todoPath = join27(outDir, "VERIFY.todo.json");
|
|
47386
|
+
const mdPath = join27(outDir, "VERIFY.md");
|
|
47387
|
+
writeFileSync8(todoPath, JSON.stringify(items, null, 2) + "\n");
|
|
47388
|
+
writeFileSync8(mdPath, formatWorklist(items, semantic, standard, lang));
|
|
46164
47389
|
return { todoPath, mdPath, count: items.length };
|
|
46165
47390
|
}
|
|
46166
47391
|
|
|
46167
47392
|
// src/grounding.ts
|
|
46168
|
-
import { existsSync as
|
|
47393
|
+
import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
|
|
46169
47394
|
import { resolve as resolve4 } from "path";
|
|
46170
47395
|
var WINDOW = 10;
|
|
46171
47396
|
var norm2 = (s) => s.replace(/\s+/g, " ").trim();
|
|
@@ -46184,10 +47409,10 @@ function selectorProbes(selector) {
|
|
|
46184
47409
|
function groundFinding(g, opts = {}) {
|
|
46185
47410
|
if (isUnresolvable(g.file)) return { ok: true, moved: false };
|
|
46186
47411
|
const path = resolve4(opts.cwd ?? process.cwd(), g.file);
|
|
46187
|
-
if (!
|
|
47412
|
+
if (!existsSync12(path)) return { ok: false, moved: false, issue: `cited file not found: ${g.file}` };
|
|
46188
47413
|
let text;
|
|
46189
47414
|
try {
|
|
46190
|
-
text =
|
|
47415
|
+
text = readFileSync13(path, "utf8");
|
|
46191
47416
|
} catch {
|
|
46192
47417
|
return { ok: false, moved: false, issue: `cited file unreadable: ${g.file}` };
|
|
46193
47418
|
}
|
|
@@ -46300,14 +47525,14 @@ function checkReport(md, standard = "wcag", lang = "en", opts = {}) {
|
|
|
46300
47525
|
if (!rateM) {
|
|
46301
47526
|
issues.push(s.rateMissing);
|
|
46302
47527
|
} else {
|
|
46303
|
-
const
|
|
46304
|
-
if (
|
|
47528
|
+
const pct2 = parseFloat(rateM[1].replace(",", "."));
|
|
47529
|
+
if (pct2 < 0 || pct2 > 100) issues.push(s.rateRange(rateM[1]));
|
|
46305
47530
|
else if (core) {
|
|
46306
47531
|
const totals = synthesisTotals(md);
|
|
46307
47532
|
if (totals) {
|
|
46308
47533
|
const { c: c2, nc } = totals;
|
|
46309
47534
|
const expected = c2 + nc === 0 ? 100 : Math.round(c2 / (c2 + nc) * 100);
|
|
46310
|
-
if (Math.abs(
|
|
47535
|
+
if (Math.abs(pct2 - expected) > 1) issues.push(s.rateInconsistent(rateM[1], expected, c2, nc));
|
|
46311
47536
|
}
|
|
46312
47537
|
}
|
|
46313
47538
|
}
|
|
@@ -46334,11 +47559,11 @@ function checkSemantic(md, opts) {
|
|
|
46334
47559
|
const standard = opts.standard ?? "wcag";
|
|
46335
47560
|
const s = M[lang];
|
|
46336
47561
|
const empty = { total: 0, grounded: 0, moved: 0, failed: 0 };
|
|
46337
|
-
const artifact = opts.verdictsPath ??
|
|
46338
|
-
if (!
|
|
47562
|
+
const artifact = opts.verdictsPath ?? join28(dirname9(opts.reportPath), "VERIFY.todo.json");
|
|
47563
|
+
if (!existsSync13(artifact)) return { ok: false, issues: [s.semanticMissing(artifact)], ...empty };
|
|
46339
47564
|
let items;
|
|
46340
47565
|
try {
|
|
46341
|
-
const parsed = JSON.parse(
|
|
47566
|
+
const parsed = JSON.parse(readFileSync14(artifact, "utf8"));
|
|
46342
47567
|
if (!Array.isArray(parsed)) throw new Error("not an array");
|
|
46343
47568
|
items = parsed;
|
|
46344
47569
|
} catch {
|
|
@@ -46370,8 +47595,8 @@ function sectionBody(md, n) {
|
|
|
46370
47595
|
}
|
|
46371
47596
|
|
|
46372
47597
|
// src/adjudicate.ts
|
|
46373
|
-
import { mkdirSync as
|
|
46374
|
-
import { join as
|
|
47598
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
47599
|
+
import { join as join29 } from "path";
|
|
46375
47600
|
|
|
46376
47601
|
// src/data/adjudication.json
|
|
46377
47602
|
var adjudication_default = {
|
|
@@ -47294,49 +48519,91 @@ function docsForAudit(audit2, cwd) {
|
|
|
47294
48519
|
const docs = [];
|
|
47295
48520
|
for (const f of files) {
|
|
47296
48521
|
try {
|
|
47297
|
-
docs.push(parseSource(readText(cwd ?
|
|
48522
|
+
docs.push(parseSource(readText(cwd ? join29(cwd, f) : f), f));
|
|
47298
48523
|
} catch {
|
|
47299
48524
|
}
|
|
47300
48525
|
}
|
|
47301
48526
|
return docs;
|
|
47302
48527
|
}
|
|
48528
|
+
function blankItem(criteriaId, automatability, title2, harvested) {
|
|
48529
|
+
const evidence = harvested.slice(0, ADJUDICATE_MAX_EVIDENCE);
|
|
48530
|
+
return {
|
|
48531
|
+
criteriaId,
|
|
48532
|
+
automatability,
|
|
48533
|
+
...title2 ? { title: title2 } : {},
|
|
48534
|
+
evidence,
|
|
48535
|
+
...harvested.length > ADJUDICATE_MAX_EVIDENCE ? { evidenceTruncated: { shown: evidence.length, total: harvested.length } } : {},
|
|
48536
|
+
verdict: null,
|
|
48537
|
+
justification: "",
|
|
48538
|
+
reason: null,
|
|
48539
|
+
findings: [],
|
|
48540
|
+
recommendations: [],
|
|
48541
|
+
decidedBy: "agent"
|
|
48542
|
+
};
|
|
48543
|
+
}
|
|
48544
|
+
function packEvidence(scs, docs) {
|
|
48545
|
+
const out2 = [];
|
|
48546
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48547
|
+
for (const sc of scs) {
|
|
48548
|
+
for (const e of HARVESTERS[sc]?.(docs) ?? []) {
|
|
48549
|
+
const key = `${e.file}:${e.line}:${e.selector}`;
|
|
48550
|
+
if (seen.has(key)) continue;
|
|
48551
|
+
seen.add(key);
|
|
48552
|
+
out2.push(e);
|
|
48553
|
+
}
|
|
48554
|
+
}
|
|
48555
|
+
return out2;
|
|
48556
|
+
}
|
|
47303
48557
|
function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
47304
48558
|
const docs = docsForAudit(audit2, opts.cwd);
|
|
47305
|
-
|
|
47306
|
-
|
|
47307
|
-
const
|
|
47308
|
-
return {
|
|
47309
|
-
|
|
47310
|
-
|
|
47311
|
-
|
|
47312
|
-
|
|
47313
|
-
|
|
47314
|
-
|
|
47315
|
-
|
|
47316
|
-
|
|
47317
|
-
|
|
47318
|
-
|
|
47319
|
-
decidedBy: "agent"
|
|
47320
|
-
};
|
|
47321
|
-
});
|
|
48559
|
+
const standard = opts.standard;
|
|
48560
|
+
if (standard !== void 0 && !isCore(standard)) {
|
|
48561
|
+
const pack = loadPack(standard);
|
|
48562
|
+
return derivePackResults(audit2, standard).filter((pc) => pc.status === "manual").map((pc) => {
|
|
48563
|
+
const crit = getCriterion(pack, pc.id);
|
|
48564
|
+
const scs = crit?.wcag ?? pc.scs;
|
|
48565
|
+
const autos = scs.map((sc) => getSC(sc)?.automatability).filter((a) => !!a);
|
|
48566
|
+
const automatability = autos.includes("needs-rendering") ? "needs-rendering" : "judgment";
|
|
48567
|
+
return blankItem(pc.id, automatability, crit ? titlePlain(pack, crit, "fr") : void 0, packEvidence(scs, docs));
|
|
48568
|
+
});
|
|
48569
|
+
}
|
|
48570
|
+
return audit2.residualRisks.map(
|
|
48571
|
+
(r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, HARVESTERS[r.criteriaId]?.(docs) ?? [])
|
|
48572
|
+
);
|
|
47322
48573
|
}
|
|
47323
48574
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
47324
48575
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
47325
|
-
function normativeRefResolves(ref, standard) {
|
|
48576
|
+
function normativeRefResolves(ref, standard, itemCriterionId) {
|
|
47326
48577
|
const r = (ref ?? "").trim();
|
|
47327
48578
|
if (!r) return false;
|
|
47328
48579
|
if (isCore(standard)) return hasSC(r);
|
|
47329
48580
|
const pack = loadPack(standard);
|
|
47330
|
-
|
|
47331
|
-
|
|
47332
|
-
if (
|
|
47333
|
-
|
|
47334
|
-
|
|
48581
|
+
let critId = hasId(pack, r) ? r : void 0;
|
|
48582
|
+
let testKey;
|
|
48583
|
+
if (critId === void 0) {
|
|
48584
|
+
const dot = r.lastIndexOf(".");
|
|
48585
|
+
if (dot <= 0) return false;
|
|
48586
|
+
const head = r.slice(0, dot);
|
|
48587
|
+
if (!hasId(pack, head)) return false;
|
|
48588
|
+
critId = head;
|
|
48589
|
+
testKey = r.slice(dot + 1);
|
|
48590
|
+
}
|
|
48591
|
+
const crit = getCriterion(pack, critId);
|
|
48592
|
+
if (!crit) return false;
|
|
48593
|
+
if (testKey !== void 0 && !(crit.tests && Object.hasOwn(crit.tests, testKey))) return false;
|
|
48594
|
+
return itemCriterionId === void 0 || critId === itemCriterionId;
|
|
47335
48595
|
}
|
|
47336
48596
|
function applyAdjudication(audit2, adj, opts = {}) {
|
|
47337
48597
|
const issues = [];
|
|
47338
48598
|
const byId2 = new Map(adj.items.map((it) => [it.criteriaId, it]));
|
|
47339
|
-
|
|
48599
|
+
const packMode = !isCore(adj.standard);
|
|
48600
|
+
if (packMode) {
|
|
48601
|
+
for (const pc of derivePackResults(audit2, adj.standard)) {
|
|
48602
|
+
if (pc.status === "manual" && !byId2.has(pc.id)) issues.push(`criterion ${pc.id}: missing from the adjudication (coverage gap)`);
|
|
48603
|
+
}
|
|
48604
|
+
} else {
|
|
48605
|
+
for (const r of audit2.residualRisks) if (!byId2.has(r.criteriaId)) issues.push(`criterion ${r.criteriaId}: missing from the adjudication (coverage gap)`);
|
|
48606
|
+
}
|
|
47340
48607
|
const groundInputs = [];
|
|
47341
48608
|
for (const it of adj.items) {
|
|
47342
48609
|
const v = it.verdict;
|
|
@@ -47349,8 +48616,10 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
47349
48616
|
for (const f of it.findings ?? []) {
|
|
47350
48617
|
if (!f.normativeRef || !f.normativeRef.trim()) {
|
|
47351
48618
|
issues.push(`criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
|
|
47352
|
-
} else if (!normativeRefResolves(f.normativeRef, adj.standard)) {
|
|
47353
|
-
issues.push(
|
|
48619
|
+
} else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
|
|
48620
|
+
issues.push(
|
|
48621
|
+
isCore(adj.standard) ? `criterion ${it.criteriaId}: normativeRef "${f.normativeRef}" does not resolve to a test of ${adj.standard} (fabricated?)` : `criterion ${it.criteriaId}: normativeRef "${f.normativeRef}" is not a test of ${adj.standard} ${it.criteriaId} \u2014 cite one of its own tests (e.g. "${it.criteriaId}.1"); a WCAG id looks alike but denotes an unrelated test`
|
|
48622
|
+
);
|
|
47354
48623
|
}
|
|
47355
48624
|
groundInputs.push({ file: f.file, line: f.line, selector: f.selector, snippet: f.snippet });
|
|
47356
48625
|
}
|
|
@@ -47372,6 +48641,38 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
47372
48641
|
const newFindings = [];
|
|
47373
48642
|
let applied = 0;
|
|
47374
48643
|
let stillManual = 0;
|
|
48644
|
+
if (packMode) {
|
|
48645
|
+
const decided = [];
|
|
48646
|
+
for (const it of adj.items) {
|
|
48647
|
+
if (it.verdict === "manual") {
|
|
48648
|
+
stillManual++;
|
|
48649
|
+
decided.push({
|
|
48650
|
+
id: it.criteriaId,
|
|
48651
|
+
status: "manual",
|
|
48652
|
+
reason: it.reason === "needs-rendered-dom" ? "needs-rendered-dom" : "undecidable",
|
|
48653
|
+
justification: it.reason === "needs-rendered-dom" ? residualScanReason() : residualUndecidableReason(),
|
|
48654
|
+
findings: [],
|
|
48655
|
+
decidedBy: "agent"
|
|
48656
|
+
});
|
|
48657
|
+
continue;
|
|
48658
|
+
}
|
|
48659
|
+
applied++;
|
|
48660
|
+
const fs2 = it.verdict === "NC" ? it.findings.map((f) => agentFinding(it.criteriaId, f)) : [];
|
|
48661
|
+
const recs = (it.recommendations ?? []).map((rec) => agentFinding(it.criteriaId, rec, true));
|
|
48662
|
+
newFindings.push(...fs2, ...recs);
|
|
48663
|
+
decided.push({
|
|
48664
|
+
id: it.criteriaId,
|
|
48665
|
+
status: it.verdict,
|
|
48666
|
+
...it.verdict === "C" || it.verdict === "NA" ? { justification: it.justification.trim() } : {},
|
|
48667
|
+
findings: [...fs2, ...recs],
|
|
48668
|
+
decidedBy: "agent"
|
|
48669
|
+
});
|
|
48670
|
+
}
|
|
48671
|
+
next.packAdjudication = { standard: adj.standard, criteria: decided };
|
|
48672
|
+
next.findings = [...next.findings, ...newFindings];
|
|
48673
|
+
next.adjudicated = { date: adj.auditDate, applied, stillManual };
|
|
48674
|
+
return { ok: true, audit: next, issues, applied, stillManual, grounding };
|
|
48675
|
+
}
|
|
47375
48676
|
for (const it of adj.items) {
|
|
47376
48677
|
const c2 = critById.get(it.criteriaId);
|
|
47377
48678
|
if (!c2) continue;
|
|
@@ -47454,7 +48755,12 @@ var T2 = {
|
|
|
47454
48755
|
questions: "\xC0 v\xE9rifier manuellement",
|
|
47455
48756
|
decide: "R\xE8gle de d\xE9cision",
|
|
47456
48757
|
na: "Non applicable si",
|
|
47457
|
-
refs: "R\xE9f\xE9rences normatives mobilisables (techniques/\xE9checs W3C de ce crit\xE8re)"
|
|
48758
|
+
refs: "R\xE9f\xE9rences normatives mobilisables (techniques/\xE9checs W3C de ce crit\xE8re)",
|
|
48759
|
+
packIntro: (name2) => `R\xE9f\xE9rentiel actif : **${name2}**. Les items ci-dessous sont des crit\xE8res ${name2}, pas des crit\xE8res de succ\xE8s WCAG. Un \`normativeRef\` DOIT citer un test du crit\xE8re de l'item (par ex. \`11.2.1\`) \u2014 un id WCAG y ressemble mais d\xE9signe un tout autre test et sera rejet\xE9.`,
|
|
48760
|
+
packTests: (name2, id) => `Tests ${name2} ${id} \xE0 trancher`,
|
|
48761
|
+
technicalNote: "Note technique",
|
|
48762
|
+
particularCases: "Cas particuliers",
|
|
48763
|
+
glossary: "Termes d\xE9finis par le r\xE9f\xE9rentiel"
|
|
47458
48764
|
},
|
|
47459
48765
|
en: {
|
|
47460
48766
|
title: "# Criteria adjudication (ultra11y)",
|
|
@@ -47472,16 +48778,60 @@ var T2 = {
|
|
|
47472
48778
|
questions: "To verify manually",
|
|
47473
48779
|
decide: "Decision rule",
|
|
47474
48780
|
na: "Not applicable when",
|
|
47475
|
-
refs: "Normative references you may cite (this criterion's W3C techniques/failures)"
|
|
48781
|
+
refs: "Normative references you may cite (this criterion's W3C techniques/failures)",
|
|
48782
|
+
packIntro: (name2) => `Active standard: **${name2}**. The items below are ${name2} criteria, not WCAG success criteria. A \`normativeRef\` MUST cite a test OF THE ITEM'S CRITERION (e.g. \`11.2.1\`) \u2014 a WCAG id looks alike but denotes an unrelated test and will be rejected.`,
|
|
48783
|
+
packTests: (name2, id) => `${name2} ${id} tests to rule on`,
|
|
48784
|
+
technicalNote: "Technical note",
|
|
48785
|
+
particularCases: "Particular cases",
|
|
48786
|
+
glossary: "Terms the standard defines"
|
|
47476
48787
|
}
|
|
47477
48788
|
};
|
|
47478
48789
|
var ADJUDICATION = adjudication_default;
|
|
47479
48790
|
var MAX_REFS = 12;
|
|
47480
|
-
|
|
48791
|
+
var GLOSSARY_REF = /\[[^\]]+\]\(#([^)]+)\)/g;
|
|
48792
|
+
var MAX_GLOSSARY_TERMS = 8;
|
|
48793
|
+
var MAX_GLOSSARY_CHARS = 600;
|
|
48794
|
+
function glossaryAnchorsOf(crit) {
|
|
48795
|
+
if (!crit) return [];
|
|
48796
|
+
const texts = [...Object.values(crit.tests ?? {}).flat(), ...crit.technicalNote ?? [], ...crit.particularCases ?? []];
|
|
48797
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48798
|
+
for (const t2 of texts) {
|
|
48799
|
+
GLOSSARY_REF.lastIndex = 0;
|
|
48800
|
+
for (let m = GLOSSARY_REF.exec(t2); m; m = GLOSSARY_REF.exec(t2)) if (m[1]) seen.add(m[1]);
|
|
48801
|
+
}
|
|
48802
|
+
return [...seen];
|
|
48803
|
+
}
|
|
48804
|
+
function glossaryBlock(pack, crit, lang) {
|
|
48805
|
+
const anchors = glossaryAnchorsOf(crit).slice(0, MAX_GLOSSARY_TERMS);
|
|
48806
|
+
if (!anchors.length) return [];
|
|
47481
48807
|
const s = T2[lang];
|
|
48808
|
+
const out2 = [`> **${s.glossary}**`, ""];
|
|
48809
|
+
let any = false;
|
|
48810
|
+
for (const a of anchors) {
|
|
48811
|
+
const entry = resolveGlossary(pack.key, a);
|
|
48812
|
+
if (!entry) continue;
|
|
48813
|
+
any = true;
|
|
48814
|
+
const body2 = entry.body.replace(/\s+/g, " ").trim();
|
|
48815
|
+
out2.push(`- **${entry.title}** \u2014 ${body2.length > MAX_GLOSSARY_CHARS ? `${body2.slice(0, MAX_GLOSSARY_CHARS)}\u2026` : body2}`);
|
|
48816
|
+
}
|
|
48817
|
+
out2.push("");
|
|
48818
|
+
return any ? out2 : [];
|
|
48819
|
+
}
|
|
48820
|
+
function packGuidanceBlock(standard, criterionId, lang) {
|
|
48821
|
+
const entries = guidanceForCriterion(standard, criterionId);
|
|
48822
|
+
if (!entries.length) return [];
|
|
48823
|
+
return guidanceExampleBlock(entries, lang);
|
|
48824
|
+
}
|
|
48825
|
+
function plainTest(s) {
|
|
48826
|
+
return s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
|
|
48827
|
+
}
|
|
48828
|
+
function formatAdjudication(items, lang = "en", standard = CORE2) {
|
|
48829
|
+
const s = T2[lang];
|
|
48830
|
+
const pack = isCore(standard) ? void 0 : loadPack(standard);
|
|
47482
48831
|
const out2 = [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
|
|
48832
|
+
if (pack) out2.push(`> ${s.packIntro(pack.name)}`, "");
|
|
47483
48833
|
for (const it of items) {
|
|
47484
|
-
out2.push(`## ${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
|
|
48834
|
+
out2.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
|
|
47485
48835
|
out2.push("", `> ${s.evidence} (${it.evidence.length}${it.evidenceTruncated ? ` / ${it.evidenceTruncated.total}` : ""}):`, "");
|
|
47486
48836
|
if (!it.evidence.length) out2.push(s.none, "");
|
|
47487
48837
|
else {
|
|
@@ -47498,17 +48848,37 @@ function formatAdjudication(items, lang = "en") {
|
|
|
47498
48848
|
out2.push("");
|
|
47499
48849
|
}
|
|
47500
48850
|
}
|
|
47501
|
-
|
|
47502
|
-
|
|
47503
|
-
|
|
48851
|
+
if (pack) {
|
|
48852
|
+
const crit = getCriterion(pack, it.criteriaId);
|
|
48853
|
+
const tests = crit?.tests ?? {};
|
|
48854
|
+
const keys = Object.keys(tests);
|
|
48855
|
+
if (keys.length) {
|
|
48856
|
+
out2.push(`> **${s.packTests(pack.name, it.criteriaId)}**`, "");
|
|
48857
|
+
for (const k of keys) {
|
|
48858
|
+
const lines = tests[k] ?? [];
|
|
48859
|
+
out2.push(`- \`${it.criteriaId}.${k}\` ${plainTest(lines[0] ?? "")}`);
|
|
48860
|
+
for (const line of lines.slice(1)) out2.push(` - ${plainTest(line)}`);
|
|
48861
|
+
}
|
|
48862
|
+
out2.push("");
|
|
48863
|
+
}
|
|
48864
|
+
if (crit?.technicalNote?.length) out2.push(`> **${s.technicalNote}** \u2014 ${crit.technicalNote.map(plainTest).join(" ")}`, "");
|
|
48865
|
+
if (crit?.particularCases?.length) out2.push(`> **${s.particularCases}** \u2014 ${crit.particularCases.map(plainTest).join(" ")}`, "");
|
|
48866
|
+
out2.push(...glossaryBlock(pack, crit, lang));
|
|
48867
|
+
out2.push(...packGuidanceBlock(standard, it.criteriaId, lang));
|
|
48868
|
+
if (keys.length) out2.push(`> ${s.refs}: ${keys.map((k) => `\`${it.criteriaId}.${k}\``).join(", ")}`, "");
|
|
48869
|
+
} else {
|
|
48870
|
+
const refs = techniquesFor(it.criteriaId);
|
|
48871
|
+
if (refs.length) {
|
|
48872
|
+
out2.push(`> ${s.refs}: ${refs.slice(0, MAX_REFS).join(", ")}${refs.length > MAX_REFS ? ` \u2026 (\`criteria ${it.criteriaId}\`)` : ""}`, "");
|
|
48873
|
+
}
|
|
47504
48874
|
}
|
|
47505
48875
|
}
|
|
47506
48876
|
return out2.join("\n");
|
|
47507
48877
|
}
|
|
47508
48878
|
function writeAdjudication(items, outDir, opts) {
|
|
47509
|
-
|
|
47510
|
-
const todoPath =
|
|
47511
|
-
const mdPath =
|
|
48879
|
+
mkdirSync8(outDir, { recursive: true });
|
|
48880
|
+
const todoPath = join29(outDir, "ADJUDICATE.todo.json");
|
|
48881
|
+
const mdPath = join29(outDir, "ADJUDICATE.md");
|
|
47512
48882
|
const file = {
|
|
47513
48883
|
tool: "ultra11y",
|
|
47514
48884
|
kind: "adjudication",
|
|
@@ -47517,16 +48887,16 @@ function writeAdjudication(items, outDir, opts) {
|
|
|
47517
48887
|
auditDate: opts.auditDate,
|
|
47518
48888
|
items
|
|
47519
48889
|
};
|
|
47520
|
-
|
|
47521
|
-
|
|
48890
|
+
writeFileSync9(todoPath, JSON.stringify(file, null, 2) + "\n");
|
|
48891
|
+
writeFileSync9(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
|
|
47522
48892
|
return { todoPath, mdPath, count: items.length };
|
|
47523
48893
|
}
|
|
47524
48894
|
|
|
47525
48895
|
// src/scan.ts
|
|
47526
48896
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
47527
|
-
import { mkdtempSync as mkdtempSync2, writeFileSync as
|
|
48897
|
+
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync10, existsSync as existsSync15, statSync as statSync7, readdirSync as readdirSync5, rmSync as rmSync3, readFileSync as readFileSync15 } from "fs";
|
|
47528
48898
|
import { tmpdir } from "os";
|
|
47529
|
-
import { join as
|
|
48899
|
+
import { join as join30, resolve as resolve5 } from "path";
|
|
47530
48900
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
47531
48901
|
|
|
47532
48902
|
// src/axe-map.ts
|
|
@@ -47768,7 +49138,7 @@ async function crawlUrls(start2, opts) {
|
|
|
47768
49138
|
}
|
|
47769
49139
|
|
|
47770
49140
|
// src/sample.ts
|
|
47771
|
-
import { existsSync as
|
|
49141
|
+
import { existsSync as existsSync14 } from "fs";
|
|
47772
49142
|
function looksLikeTarget(u) {
|
|
47773
49143
|
return /^https?:\/\//i.test(u) || /^(\.\.?[/\\]|[/\\])/.test(u) || /\.x?html?$/i.test(u);
|
|
47774
49144
|
}
|
|
@@ -47804,7 +49174,7 @@ function validateSample(raw) {
|
|
|
47804
49174
|
if (p.auth !== void 0 && typeof p.auth !== "boolean") err2(`sample.pages[${i2}].auth`, "auth must be a boolean");
|
|
47805
49175
|
if (p.storageState !== void 0 && (typeof p.storageState !== "string" || p.storageState.trim() === "")) {
|
|
47806
49176
|
err2(`sample.pages[${i2}].storageState`, "storageState must be a non-empty file path string");
|
|
47807
|
-
} else if (typeof p.storageState === "string" && p.storageState.trim() !== "" && !
|
|
49177
|
+
} else if (typeof p.storageState === "string" && p.storageState.trim() !== "" && !existsSync14(p.storageState)) {
|
|
47808
49178
|
warn(
|
|
47809
49179
|
`sample.pages[${i2}].storageState`,
|
|
47810
49180
|
`storageState path not found: "${p.storageState}" (resolved from the current directory) \u2014 the scan will not be able to load this session`
|
|
@@ -47929,11 +49299,11 @@ function imageExists(tag) {
|
|
|
47929
49299
|
}
|
|
47930
49300
|
var CTX_PREFIX = "ultra11y-dyn-";
|
|
47931
49301
|
function buildImage(tag = IMAGE_TAG) {
|
|
47932
|
-
const ctx = mkdtempSync2(
|
|
49302
|
+
const ctx = mkdtempSync2(join30(tmpdir(), CTX_PREFIX));
|
|
47933
49303
|
try {
|
|
47934
|
-
|
|
47935
|
-
|
|
47936
|
-
|
|
49304
|
+
writeFileSync10(join30(ctx, "runner.mjs"), RUNNER);
|
|
49305
|
+
writeFileSync10(join30(ctx, "package.json"), PKG);
|
|
49306
|
+
writeFileSync10(join30(ctx, "Dockerfile"), DOCKERFILE);
|
|
47937
49307
|
execFileSync3("docker", ["build", "-t", tag, ctx], { stdio: "inherit", timeout: 9e5 });
|
|
47938
49308
|
} finally {
|
|
47939
49309
|
rmSync3(ctx, { recursive: true, force: true });
|
|
@@ -47942,9 +49312,9 @@ function buildImage(tag = IMAGE_TAG) {
|
|
|
47942
49312
|
function cleanTempContexts() {
|
|
47943
49313
|
let removed = 0;
|
|
47944
49314
|
const dir = tmpdir();
|
|
47945
|
-
for (const name2 of
|
|
49315
|
+
for (const name2 of readdirSync5(dir)) {
|
|
47946
49316
|
if (!name2.startsWith(CTX_PREFIX)) continue;
|
|
47947
|
-
rmSync3(
|
|
49317
|
+
rmSync3(join30(dir, name2), { recursive: true, force: true });
|
|
47948
49318
|
removed++;
|
|
47949
49319
|
}
|
|
47950
49320
|
return removed;
|
|
@@ -48054,8 +49424,8 @@ function toDynamicResult(out2, target, lang = "en", engine = "axe-core@playwrigh
|
|
|
48054
49424
|
}
|
|
48055
49425
|
var DOCKER_TESTED_SCS = ["1.4.10"];
|
|
48056
49426
|
function runScan(opts) {
|
|
48057
|
-
const
|
|
48058
|
-
if (!
|
|
49427
|
+
const isUrl3 = /^https?:\/\//i.test(opts.target);
|
|
49428
|
+
if (!isUrl3 && !existsSync15(opts.target)) {
|
|
48059
49429
|
throw new Error(`File not found: ${opts.target}. Pass an http(s):// URL or an existing HTML file.`);
|
|
48060
49430
|
}
|
|
48061
49431
|
if (!dockerAvailable()) {
|
|
@@ -48063,7 +49433,7 @@ function runScan(opts) {
|
|
|
48063
49433
|
}
|
|
48064
49434
|
const tag = opts.tag ?? IMAGE_TAG;
|
|
48065
49435
|
if (!imageExists(tag)) buildImage(tag);
|
|
48066
|
-
const isFile = !
|
|
49436
|
+
const isFile = !isUrl3 && existsSync15(opts.target) && statSync7(opts.target).isFile();
|
|
48067
49437
|
const out2 = runRunner(opts.target, isFile, tag);
|
|
48068
49438
|
return { ...toDynamicResult(out2, opts.target), testedScs: [...DOCKER_TESTED_SCS] };
|
|
48069
49439
|
}
|
|
@@ -48141,10 +49511,10 @@ async function runCrawlScan(opts) {
|
|
|
48141
49511
|
var sevRank = { bloquant: 3, majeur: 2, mineur: 1 };
|
|
48142
49512
|
function resolveHostAnchor(file, snippet2) {
|
|
48143
49513
|
const s = snippet2?.trim();
|
|
48144
|
-
if (!s || !
|
|
49514
|
+
if (!s || !existsSync15(file)) return null;
|
|
48145
49515
|
let source;
|
|
48146
49516
|
try {
|
|
48147
|
-
source =
|
|
49517
|
+
source = readFileSync15(file, "utf8");
|
|
48148
49518
|
} catch {
|
|
48149
49519
|
return null;
|
|
48150
49520
|
}
|
|
@@ -48240,7 +49610,7 @@ function mergeDynamic(audit2, dynamic, lang = "en") {
|
|
|
48240
49610
|
}
|
|
48241
49611
|
|
|
48242
49612
|
// src/scan-local.ts
|
|
48243
|
-
import { existsSync as
|
|
49613
|
+
import { existsSync as existsSync16, statSync as statSync8 } from "fs";
|
|
48244
49614
|
import { createRequire } from "module";
|
|
48245
49615
|
import { resolve as resolve6 } from "path";
|
|
48246
49616
|
var LOCAL_ENGINE = "axe-core@playwright (local)";
|
|
@@ -48795,11 +50165,11 @@ async function runOnPage(browser, AxeBuilder, target, isFile, opts) {
|
|
|
48795
50165
|
}
|
|
48796
50166
|
}
|
|
48797
50167
|
async function runScanLocal(opts) {
|
|
48798
|
-
const
|
|
48799
|
-
if (!
|
|
50168
|
+
const isUrl3 = /^https?:\/\//i.test(opts.target);
|
|
50169
|
+
if (!isUrl3 && !existsSync16(opts.target)) {
|
|
48800
50170
|
throw new Error(`File not found: ${opts.target}. Pass an http(s):// URL or an existing HTML file.`);
|
|
48801
50171
|
}
|
|
48802
|
-
const isFile = !
|
|
50172
|
+
const isFile = !isUrl3 && statSync8(opts.target).isFile();
|
|
48803
50173
|
const lang = opts.lang ?? "en";
|
|
48804
50174
|
const interact = opts.interact !== false;
|
|
48805
50175
|
const { chromium, AxeBuilder } = resolveLocalDeps(opts.cwd);
|
|
@@ -48877,7 +50247,7 @@ async function runCrawlScanLocal(opts) {
|
|
|
48877
50247
|
}
|
|
48878
50248
|
|
|
48879
50249
|
// src/fix.ts
|
|
48880
|
-
import { writeFileSync as
|
|
50250
|
+
import { writeFileSync as writeFileSync11 } from "fs";
|
|
48881
50251
|
|
|
48882
50252
|
// src/fix/edits.ts
|
|
48883
50253
|
function openTag(source, start2) {
|
|
@@ -49091,7 +50461,7 @@ function fixOne(file, source, opts, canWrite = true) {
|
|
|
49091
50461
|
regression = after.length > findings.length || after.some((f) => !beforeKinds.has(f.ruleId));
|
|
49092
50462
|
if (opts.write && !regression && file !== "<stdin>") {
|
|
49093
50463
|
if (canWrite) {
|
|
49094
|
-
|
|
50464
|
+
writeFileSync11(file, output);
|
|
49095
50465
|
written = true;
|
|
49096
50466
|
} else {
|
|
49097
50467
|
skippedPartialStage = true;
|
|
@@ -49214,9 +50584,9 @@ function baselineSummary(diff, lang = "fr") {
|
|
|
49214
50584
|
}
|
|
49215
50585
|
|
|
49216
50586
|
// src/init.ts
|
|
49217
|
-
import { writeFileSync as
|
|
50587
|
+
import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync9, chmodSync } from "fs";
|
|
49218
50588
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
49219
|
-
import { join as
|
|
50589
|
+
import { join as join31 } from "path";
|
|
49220
50590
|
var EN_SEV = { bloquant: "blocking", majeur: "major", mineur: "minor" };
|
|
49221
50591
|
function repoRoot() {
|
|
49222
50592
|
try {
|
|
@@ -49269,35 +50639,61 @@ function ciWorkflow(enginePath, failOn) {
|
|
|
49269
50639
|
return `name: a11y
|
|
49270
50640
|
# Generated by \`ultra11y init --ci\`. Fails only on NEW non-conformities >= ${EN_SEV[failOn]}
|
|
49271
50641
|
# introduced by the PR (vs audits/baseline.json) \u2014 not the existing backlog.
|
|
50642
|
+
#
|
|
50643
|
+
# Findings are reported IN the pull request: SARIF \u2192 code scanning puts each one on the line
|
|
50644
|
+
# of code that caused it, and \`::error::\` annotations cover repositories without Advanced
|
|
50645
|
+
# Security. Set \`comment: 'true'\` to also get a sticky summary comment (needs
|
|
50646
|
+
# \`pull-requests: write\`), and \`standard: 'rgaa'\` to report against a country standard.
|
|
50647
|
+
#
|
|
50648
|
+
# The action is PINNED to the engine version that generated this file, so a CI run stays
|
|
50649
|
+
# reproducible. \`maxgfr/ultra11y@v${VERSION.split(".")[0]}\` also resolves (a moving major
|
|
50650
|
+
# tag) if you would rather track the major line and take fixes automatically.
|
|
49272
50651
|
on:
|
|
49273
50652
|
pull_request:
|
|
50653
|
+
|
|
50654
|
+
permissions:
|
|
50655
|
+
contents: read
|
|
50656
|
+
security-events: write # SARIF upload; drop it and the annotations still report
|
|
50657
|
+
# pull-requests: write # uncomment together with \`comment: 'true'\` below
|
|
50658
|
+
|
|
49274
50659
|
jobs:
|
|
49275
50660
|
ultra11y:
|
|
49276
50661
|
runs-on: ubuntu-latest
|
|
49277
50662
|
steps:
|
|
49278
50663
|
- uses: actions/checkout@v4
|
|
49279
50664
|
with:
|
|
49280
|
-
fetch-depth: 0
|
|
49281
|
-
- uses:
|
|
50665
|
+
fetch-depth: 0 # the diff gate needs the base ref
|
|
50666
|
+
- uses: maxgfr/ultra11y@v${VERSION}
|
|
49282
50667
|
with:
|
|
49283
|
-
|
|
49284
|
-
|
|
49285
|
-
|
|
50668
|
+
since: auto # the PR's base branch
|
|
50669
|
+
baseline: audits/baseline.json
|
|
50670
|
+
fail-on: ${EN_SEV[failOn]}
|
|
50671
|
+
# standard: rgaa
|
|
50672
|
+
# comment: 'true'
|
|
50673
|
+
#
|
|
50674
|
+
# Page-by-page, with a real browser (computed contrast, focus, zoom, reflow):
|
|
50675
|
+
# start: npm run start
|
|
50676
|
+
# wait-on: http://localhost:3000
|
|
50677
|
+
# urls: http://localhost:3000 http://localhost:3000/contact
|
|
50678
|
+
# \u2026or \`sample: 'true'\` to scan the sample declared in .ultra11yrc.json.
|
|
50679
|
+
#
|
|
50680
|
+
# Vendored alternative \u2014 no marketplace action, just the bundle in this repo:
|
|
50681
|
+
# - run: node "${enginePath}" audit --since "origin/\${{ github.base_ref }}" --baseline audits/baseline.json --fail-on ${EN_SEV[failOn]}
|
|
49286
50682
|
`;
|
|
49287
50683
|
}
|
|
49288
50684
|
function writeHook(root, enginePath, failOn, mode = "staged") {
|
|
49289
|
-
const dir =
|
|
49290
|
-
|
|
49291
|
-
const path =
|
|
49292
|
-
|
|
50685
|
+
const dir = join31(root, ".git", "hooks");
|
|
50686
|
+
mkdirSync9(dir, { recursive: true });
|
|
50687
|
+
const path = join31(dir, "pre-commit");
|
|
50688
|
+
writeFileSync12(path, mode === "baseline" ? hookScript(enginePath, failOn) : stagedHookScript(enginePath, failOn));
|
|
49293
50689
|
chmodSync(path, 493);
|
|
49294
50690
|
return path;
|
|
49295
50691
|
}
|
|
49296
50692
|
function writeCi(root, enginePath, failOn) {
|
|
49297
|
-
const dir =
|
|
49298
|
-
|
|
49299
|
-
const path =
|
|
49300
|
-
|
|
50693
|
+
const dir = join31(root, ".github", "workflows");
|
|
50694
|
+
mkdirSync9(dir, { recursive: true });
|
|
50695
|
+
const path = join31(dir, "a11y.yml");
|
|
50696
|
+
writeFileSync12(path, ciWorkflow(enginePath, failOn));
|
|
49301
50697
|
return path;
|
|
49302
50698
|
}
|
|
49303
50699
|
|
|
@@ -49380,12 +50776,826 @@ function captureCoverageSummary(cov, lang) {
|
|
|
49380
50776
|
return lines.join("\n");
|
|
49381
50777
|
}
|
|
49382
50778
|
|
|
50779
|
+
// src/sarif.ts
|
|
50780
|
+
var INFO_URI = "https://github.com/maxgfr/ultra11y";
|
|
50781
|
+
var SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
50782
|
+
var FINGERPRINT_KEY = "ultra11yFindingId/v1";
|
|
50783
|
+
var LEVEL = { bloquant: "error", majeur: "warning", mineur: "note" };
|
|
50784
|
+
function levelFor(f) {
|
|
50785
|
+
return f.advisory ? "note" : LEVEL[f.severity];
|
|
50786
|
+
}
|
|
50787
|
+
var toUri = repoRelative;
|
|
50788
|
+
var isUrl = isUrlPath;
|
|
50789
|
+
function physicalLocation(f, baseDir) {
|
|
50790
|
+
if (isUrl(f.file)) return [];
|
|
50791
|
+
return [
|
|
50792
|
+
{
|
|
50793
|
+
physicalLocation: {
|
|
50794
|
+
artifactLocation: { uri: toUri(f.file, baseDir) },
|
|
50795
|
+
region: {
|
|
50796
|
+
// SARIF regions are 1-based. A merged dynamic finding can carry line 0 when the
|
|
50797
|
+
// host-anchor resolver declined to guess (src/scan.ts resolveHostAnchor) — clamp
|
|
50798
|
+
// rather than emit an invalid region.
|
|
50799
|
+
startLine: Math.max(1, f.line),
|
|
50800
|
+
startColumn: Math.max(1, f.col),
|
|
50801
|
+
...f.snippet ? { snippet: { text: f.snippet } } : {}
|
|
50802
|
+
}
|
|
50803
|
+
}
|
|
50804
|
+
}
|
|
50805
|
+
];
|
|
50806
|
+
}
|
|
50807
|
+
function relatedLocations(f, baseDir) {
|
|
50808
|
+
const r = f.related;
|
|
50809
|
+
if (!r || isUrl(r.file)) return void 0;
|
|
50810
|
+
return [
|
|
50811
|
+
{ physicalLocation: { artifactLocation: { uri: toUri(r.file, baseDir) }, region: { startLine: Math.max(1, r.line), startColumn: Math.max(1, r.col) } } }
|
|
50812
|
+
];
|
|
50813
|
+
}
|
|
50814
|
+
function criterionLabel(f, standard) {
|
|
50815
|
+
if (isCore(standard)) return `WCAG ${f.criteriaId}`;
|
|
50816
|
+
const pack = loadPack(standard);
|
|
50817
|
+
const ids = packCriteriaForFinding(pack, f);
|
|
50818
|
+
return ids.length ? `${pack.name} ${ids.join(", ")}` : `WCAG ${f.criteriaId}`;
|
|
50819
|
+
}
|
|
50820
|
+
function ruleFor(f, standard, lang) {
|
|
50821
|
+
const sc = f.criteriaId;
|
|
50822
|
+
const title2 = scTitle(sc, lang);
|
|
50823
|
+
const tags = ["accessibility", `wcag:${sc}`];
|
|
50824
|
+
if (!isCore(standard)) {
|
|
50825
|
+
const pack = loadPack(standard);
|
|
50826
|
+
for (const id of packCriteriaForFinding(pack, f)) tags.push(`${pack.key}:${id}`);
|
|
50827
|
+
}
|
|
50828
|
+
if (f.advisory) tags.push("recommendation");
|
|
50829
|
+
const level = levelFor(f);
|
|
50830
|
+
return {
|
|
50831
|
+
id: f.ruleId,
|
|
50832
|
+
shortDescription: { text: title2 ? `${f.ruleId} \u2014 WCAG ${sc} ${title2}` : `${f.ruleId} \u2014 WCAG ${sc}` },
|
|
50833
|
+
fullDescription: { text: resolveRemediation(f, lang) },
|
|
50834
|
+
...understanding(sc) ? { helpUri: understanding(sc) } : {},
|
|
50835
|
+
defaultConfiguration: { level },
|
|
50836
|
+
properties: { tags, "problem.severity": level }
|
|
50837
|
+
};
|
|
50838
|
+
}
|
|
50839
|
+
function toSarif(result, opts = {}) {
|
|
50840
|
+
const standard = opts.standard ?? CORE2;
|
|
50841
|
+
const lang = opts.lang ?? "en";
|
|
50842
|
+
const baseDir = opts.baseDir ?? process.cwd();
|
|
50843
|
+
const rules = [];
|
|
50844
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
50845
|
+
const results = [];
|
|
50846
|
+
for (const f of findingsForStandard(result, standard)) {
|
|
50847
|
+
let idx = indexOf.get(f.ruleId);
|
|
50848
|
+
if (idx === void 0) {
|
|
50849
|
+
idx = rules.length;
|
|
50850
|
+
indexOf.set(f.ruleId, idx);
|
|
50851
|
+
rules.push(ruleFor(f, standard, lang));
|
|
50852
|
+
}
|
|
50853
|
+
const related = relatedLocations(f, baseDir);
|
|
50854
|
+
const page = f.sample?.page;
|
|
50855
|
+
const properties = { criterion: criterionLabel(f, standard) };
|
|
50856
|
+
if (isUrl(f.file)) properties.url = f.file;
|
|
50857
|
+
if (page) properties.page = page;
|
|
50858
|
+
if (f.advisory) properties.advisory = true;
|
|
50859
|
+
if (f.preliminary) properties.preliminary = true;
|
|
50860
|
+
results.push({
|
|
50861
|
+
ruleId: f.ruleId,
|
|
50862
|
+
ruleIndex: idx,
|
|
50863
|
+
level: levelFor(f),
|
|
50864
|
+
message: { text: `[${criterionLabel(f, standard)}] ${resolveMessage(f, lang)} \u2014 ${resolveRemediation(f, lang)}` },
|
|
50865
|
+
locations: physicalLocation(f, baseDir),
|
|
50866
|
+
...related ? { relatedLocations: related } : {},
|
|
50867
|
+
partialFingerprints: { [FINGERPRINT_KEY]: findingId(f) },
|
|
50868
|
+
properties
|
|
50869
|
+
});
|
|
50870
|
+
}
|
|
50871
|
+
return {
|
|
50872
|
+
$schema: SCHEMA,
|
|
50873
|
+
version: "2.1.0",
|
|
50874
|
+
runs: [
|
|
50875
|
+
{
|
|
50876
|
+
tool: { driver: { name: "ultra11y", version: VERSION, informationUri: INFO_URI, rules } },
|
|
50877
|
+
// Distinguishes concurrent uploads in one PR (source audit vs page scan) so
|
|
50878
|
+
// GitHub keeps them as separate analyses instead of overwriting one another.
|
|
50879
|
+
automationDetails: { id: `ultra11y/${standard}/` },
|
|
50880
|
+
results
|
|
50881
|
+
}
|
|
50882
|
+
]
|
|
50883
|
+
};
|
|
50884
|
+
}
|
|
50885
|
+
|
|
50886
|
+
// src/annotate.ts
|
|
50887
|
+
var LEVEL2 = { bloquant: "error", majeur: "warning", mineur: "notice" };
|
|
50888
|
+
var ICON6 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
|
|
50889
|
+
var SEV_ORDER5 = ["bloquant", "majeur", "mineur"];
|
|
50890
|
+
function esc(s) {
|
|
50891
|
+
return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
50892
|
+
}
|
|
50893
|
+
function escProp(s) {
|
|
50894
|
+
return esc(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
50895
|
+
}
|
|
50896
|
+
var isUrl2 = isUrlPath;
|
|
50897
|
+
function criterionLabel2(f, standard) {
|
|
50898
|
+
if (isCore(standard)) return `WCAG ${f.criteriaId}`;
|
|
50899
|
+
const pack = loadPack(standard);
|
|
50900
|
+
const ids = packCriteriaForFinding(pack, f);
|
|
50901
|
+
return ids.length ? `${pack.name} ${ids.join(", ")}` : `WCAG ${f.criteriaId}`;
|
|
50902
|
+
}
|
|
50903
|
+
function annotations(result, opts = {}) {
|
|
50904
|
+
const standard = opts.standard ?? CORE2;
|
|
50905
|
+
const lang = opts.lang ?? "en";
|
|
50906
|
+
const baseDir = opts.baseDir ?? process.cwd();
|
|
50907
|
+
const all = findingsForStandard(result, standard);
|
|
50908
|
+
const scoped = opts.failOn ? findingsAtOrAbove(all, opts.failOn) : all;
|
|
50909
|
+
const out2 = [];
|
|
50910
|
+
for (const f of scoped) {
|
|
50911
|
+
if (isUrl2(f.file)) continue;
|
|
50912
|
+
const level = f.advisory ? "notice" : LEVEL2[f.severity];
|
|
50913
|
+
const file = repoRelative(f.file, baseDir);
|
|
50914
|
+
const title2 = `${criterionLabel2(f, standard)} \xB7 ${f.ruleId}`;
|
|
50915
|
+
const body2 = `${resolveMessage(f, lang)}
|
|
50916
|
+
${resolveRemediation(f, lang)}`;
|
|
50917
|
+
out2.push(`::${level} file=${escProp(file)},line=${Math.max(1, f.line)},col=${Math.max(1, f.col)},title=${escProp(title2)}::${esc(body2)}`);
|
|
50918
|
+
}
|
|
50919
|
+
return out2;
|
|
50920
|
+
}
|
|
50921
|
+
var S = {
|
|
50922
|
+
fr: {
|
|
50923
|
+
title: "Audit d'accessibilit\xE9 ultra11y",
|
|
50924
|
+
files: "fichiers",
|
|
50925
|
+
rate: "r\xE9ussite automatique",
|
|
50926
|
+
none: "\u2705 Aucune non-conformit\xE9 d\xE9tect\xE9e par le moteur statique.",
|
|
50927
|
+
findings: "Non-conformit\xE9s",
|
|
50928
|
+
severity: "S\xE9v\xE9rit\xE9",
|
|
50929
|
+
criterion: "Crit\xE8re",
|
|
50930
|
+
where: "Emplacement",
|
|
50931
|
+
what: "Constat",
|
|
50932
|
+
more: (n) => `\u2026 et ${n} autre(s).`,
|
|
50933
|
+
perPage: "Constats par page",
|
|
50934
|
+
page: "Page",
|
|
50935
|
+
count: "Constats",
|
|
50936
|
+
unanchored: (n) => `${n} constat(s) rattach\xE9(s) \xE0 une URL, sans ligne de code \xE0 annoter \u2014 voir le rapport.`
|
|
50937
|
+
},
|
|
50938
|
+
en: {
|
|
50939
|
+
title: "ultra11y accessibility audit",
|
|
50940
|
+
files: "files",
|
|
50941
|
+
rate: "automatic pass rate",
|
|
50942
|
+
none: "\u2705 No non-conformity detected by the static engine.",
|
|
50943
|
+
findings: "Non-conformities",
|
|
50944
|
+
severity: "Severity",
|
|
50945
|
+
criterion: "Criterion",
|
|
50946
|
+
where: "Location",
|
|
50947
|
+
what: "Finding",
|
|
50948
|
+
more: (n) => `\u2026 and ${n} more.`,
|
|
50949
|
+
perPage: "Findings per page",
|
|
50950
|
+
page: "Page",
|
|
50951
|
+
count: "Findings",
|
|
50952
|
+
unanchored: (n) => `${n} finding(s) keyed to a URL, with no code line to annotate \u2014 see the report.`
|
|
50953
|
+
}
|
|
50954
|
+
};
|
|
50955
|
+
var MAX_ROWS = 50;
|
|
50956
|
+
function stepSummary(result, opts = {}) {
|
|
50957
|
+
const standard = opts.standard ?? CORE2;
|
|
50958
|
+
const lang = opts.lang ?? "en";
|
|
50959
|
+
const s = S[lang];
|
|
50960
|
+
const stdLabel = isCore(standard) ? "WCAG 2.2 AA" : loadPack(standard).name;
|
|
50961
|
+
const out2 = [];
|
|
50962
|
+
out2.push(`## ${s.title} \u2014 ${stdLabel}`, "");
|
|
50963
|
+
out2.push(`\`${result.date}\` \xB7 ${result.scope.files} ${s.files} \xB7 **${result.conformancePct}%** ${s.rate}`, "");
|
|
50964
|
+
const baseDir = opts.baseDir ?? process.cwd();
|
|
50965
|
+
const all = findingsForStandard(result, standard);
|
|
50966
|
+
if (!all.length) {
|
|
50967
|
+
out2.push(s.none, "");
|
|
50968
|
+
return out2.join("\n");
|
|
50969
|
+
}
|
|
50970
|
+
const sorted = [...all].sort((a, b) => SEV_ORDER5.indexOf(a.severity) - SEV_ORDER5.indexOf(b.severity));
|
|
50971
|
+
out2.push(`### ${s.findings} (${sorted.length})`, "");
|
|
50972
|
+
out2.push(`| ${s.severity} | ${s.criterion} | ${s.where} | ${s.what} |`, "| --- | --- | --- | --- |");
|
|
50973
|
+
for (const f of sorted.slice(0, MAX_ROWS)) {
|
|
50974
|
+
const where = isUrl2(f.file) ? f.file : `${repoRelative(f.file, baseDir)}:${Math.max(1, f.line)}`;
|
|
50975
|
+
const msg = resolveMessage(f, lang).replace(/\|/g, "\\|");
|
|
50976
|
+
out2.push(`| ${ICON6[f.severity]} ${f.severity} | ${criterionLabel2(f, standard)} | \`${where}\` | ${msg} |`);
|
|
50977
|
+
}
|
|
50978
|
+
if (sorted.length > MAX_ROWS) out2.push("", s.more(sorted.length - MAX_ROWS));
|
|
50979
|
+
out2.push("");
|
|
50980
|
+
const unanchored = all.filter((f) => isUrl2(f.file)).length;
|
|
50981
|
+
if (unanchored) out2.push(`> ${s.unanchored(unanchored)}`, "");
|
|
50982
|
+
const pages = result.scope.sample?.pages ?? [];
|
|
50983
|
+
if (pages.length) {
|
|
50984
|
+
out2.push(`### ${s.perPage}`, "");
|
|
50985
|
+
out2.push(`| ${s.page} | ${s.count} |`, "| --- | --- |");
|
|
50986
|
+
for (const pg of pages) {
|
|
50987
|
+
const n = all.filter((f) => f.file === pg.url || f.sample?.page !== void 0 && f.sample.page === pg.name).length;
|
|
50988
|
+
out2.push(`| ${pg.name} \u2014 \`${pg.url}\` | ${n} |`);
|
|
50989
|
+
}
|
|
50990
|
+
out2.push("");
|
|
50991
|
+
}
|
|
50992
|
+
return out2.join("\n");
|
|
50993
|
+
}
|
|
50994
|
+
|
|
50995
|
+
// src/dev.ts
|
|
50996
|
+
import { createServer } from "http";
|
|
50997
|
+
import { join as join32 } from "path";
|
|
50998
|
+
var DEV_DEFAULT_PORT = 4111;
|
|
50999
|
+
function criterionLabel3(f, standard) {
|
|
51000
|
+
if (isCore(standard)) return `WCAG ${f.criteriaId}`;
|
|
51001
|
+
const pack = loadPack(standard);
|
|
51002
|
+
const ids = packCriteriaForFinding(pack, f);
|
|
51003
|
+
return ids.length ? `${pack.name} ${ids.join(", ")}` : `WCAG ${f.criteriaId}`;
|
|
51004
|
+
}
|
|
51005
|
+
function overlayJs() {
|
|
51006
|
+
return `(() => {
|
|
51007
|
+
const ENDPOINT = window.__ULTRA11Y_ENDPOINT__ || "http://127.0.0.1:${DEV_DEFAULT_PORT}";
|
|
51008
|
+
const HOST_ID = "__ultra11y_overlay";
|
|
51009
|
+
if (document.getElementById(HOST_ID)) return;
|
|
51010
|
+
|
|
51011
|
+
const host = document.createElement("div");
|
|
51012
|
+
host.id = HOST_ID;
|
|
51013
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
51014
|
+
const style = document.createElement("style");
|
|
51015
|
+
style.textContent = \`
|
|
51016
|
+
:host { all: initial; }
|
|
51017
|
+
.bar { position: fixed; right: 16px; bottom: 16px; z-index: 2147483647;
|
|
51018
|
+
font: 13px/1.4 ui-sans-serif, system-ui, sans-serif; }
|
|
51019
|
+
button.fab { border: 0; border-radius: 999px; padding: 10px 14px; cursor: pointer;
|
|
51020
|
+
background: #1c1c1e; color: #fff; box-shadow: 0 2px 12px rgba(0,0,0,.35); }
|
|
51021
|
+
button.fab[data-state="ok"] { background: #0a7d33; }
|
|
51022
|
+
button.fab[data-state="bad"] { background: #b3261e; }
|
|
51023
|
+
.panel { position: fixed; right: 16px; bottom: 64px; width: min(460px, calc(100vw - 32px));
|
|
51024
|
+
max-height: min(60vh, 560px); overflow: auto; background: #fff; color: #111;
|
|
51025
|
+
border-radius: 10px; box-shadow: 0 8px 32px rgba(0,0,0,.28); padding: 12px 14px; }
|
|
51026
|
+
@media (prefers-color-scheme: dark) { .panel { background: #1c1c1e; color: #f2f2f7; } }
|
|
51027
|
+
h2 { font-size: 13px; margin: 0 0 8px; }
|
|
51028
|
+
ul { list-style: none; margin: 0; padding: 0; }
|
|
51029
|
+
li { padding: 7px 0; border-top: 1px solid rgba(127,127,127,.25); }
|
|
51030
|
+
.sev { font-weight: 700; margin-right: 6px; }
|
|
51031
|
+
.b { color: #b3261e; } .m { color: #b06000; } .n { color: #666; }
|
|
51032
|
+
a { color: inherit; }
|
|
51033
|
+
code { font: 11px/1.4 ui-monospace, monospace; opacity: .8; }
|
|
51034
|
+
.empty { opacity: .75; }
|
|
51035
|
+
\`;
|
|
51036
|
+
const root = document.createElement("div");
|
|
51037
|
+
root.className = "bar";
|
|
51038
|
+
const fab = document.createElement("button");
|
|
51039
|
+
fab.className = "fab";
|
|
51040
|
+
fab.type = "button";
|
|
51041
|
+
fab.textContent = "a11y \u2026";
|
|
51042
|
+
const panel = document.createElement("div");
|
|
51043
|
+
panel.className = "panel";
|
|
51044
|
+
panel.hidden = true;
|
|
51045
|
+
root.append(fab, panel);
|
|
51046
|
+
shadow.append(style, root);
|
|
51047
|
+
document.body.appendChild(host);
|
|
51048
|
+
|
|
51049
|
+
fab.addEventListener("click", () => { panel.hidden = !panel.hidden; });
|
|
51050
|
+
|
|
51051
|
+
const SEV = { bloquant: ["b", "BLOQUANT"], majeur: ["m", "MAJEUR"], mineur: ["n", "mineur"] };
|
|
51052
|
+
|
|
51053
|
+
function render(findings, error) {
|
|
51054
|
+
if (error) {
|
|
51055
|
+
fab.textContent = "a11y ?";
|
|
51056
|
+
fab.dataset.state = "";
|
|
51057
|
+
panel.innerHTML = "<h2>ultra11y</h2><p class='empty'>" + String(error) + "</p>";
|
|
51058
|
+
return;
|
|
51059
|
+
}
|
|
51060
|
+
const nc = findings.filter((f) => !f.advisory);
|
|
51061
|
+
fab.textContent = nc.length ? "a11y " + nc.length : "a11y \u2713";
|
|
51062
|
+
fab.dataset.state = nc.length ? "bad" : "ok";
|
|
51063
|
+
if (!findings.length) {
|
|
51064
|
+
panel.innerHTML = "<h2>ultra11y</h2><p class='empty'>No non-conformity detected on this page by the static engine. The judgment criteria remain yours.</p>";
|
|
51065
|
+
return;
|
|
51066
|
+
}
|
|
51067
|
+
const h = document.createElement("h2");
|
|
51068
|
+
h.textContent = "ultra11y \u2014 " + nc.length + " non-conformity(ies) on this page";
|
|
51069
|
+
const ul = document.createElement("ul");
|
|
51070
|
+
for (const f of findings.slice(0, 60)) {
|
|
51071
|
+
const li = document.createElement("li");
|
|
51072
|
+
const s = SEV[f.severity] || ["n", f.severity];
|
|
51073
|
+
const sev = document.createElement("span");
|
|
51074
|
+
sev.className = "sev " + s[0];
|
|
51075
|
+
sev.textContent = f.advisory ? "reco" : s[1];
|
|
51076
|
+
li.append(sev, document.createTextNode(f.message));
|
|
51077
|
+
const where = (f.origin && f.origin.sourceFile) || f.file;
|
|
51078
|
+
const line = (f.origin && f.origin.sourceLine) || f.line || 1;
|
|
51079
|
+
if (where && !/^https?:/.test(where)) {
|
|
51080
|
+
const a = document.createElement("a");
|
|
51081
|
+
// Next exposes this endpoint in dev; it opens the file in the configured editor.
|
|
51082
|
+
a.href = "/__nextjs_launch-editor?file=" + encodeURIComponent(where + ":" + line + ":1");
|
|
51083
|
+
a.textContent = where + ":" + line;
|
|
51084
|
+
a.addEventListener("click", (e) => { e.preventDefault(); fetch(a.href).catch(() => {}); });
|
|
51085
|
+
const code = document.createElement("code");
|
|
51086
|
+
code.append(document.createElement("br"), a);
|
|
51087
|
+
li.append(code);
|
|
51088
|
+
}
|
|
51089
|
+
ul.append(li);
|
|
51090
|
+
}
|
|
51091
|
+
panel.replaceChildren(h, ul);
|
|
51092
|
+
}
|
|
51093
|
+
|
|
51094
|
+
let busy = false;
|
|
51095
|
+
async function check() {
|
|
51096
|
+
if (busy) return;
|
|
51097
|
+
busy = true;
|
|
51098
|
+
fab.textContent = "a11y \u2026";
|
|
51099
|
+
// Detach the overlay so it is neither serialized nor counted: its presence would shift
|
|
51100
|
+
// every document-order index by one and break the styles/DOM join.
|
|
51101
|
+
const parent = host.parentNode;
|
|
51102
|
+
if (parent) parent.removeChild(host);
|
|
51103
|
+
let collected;
|
|
51104
|
+
try {
|
|
51105
|
+
collected = (0, eval)(${JSON.stringify(COLLECT_SNAPSHOT)});
|
|
51106
|
+
} finally {
|
|
51107
|
+
if (parent) parent.appendChild(host);
|
|
51108
|
+
busy = false;
|
|
51109
|
+
}
|
|
51110
|
+
try {
|
|
51111
|
+
const res = await fetch(ENDPOINT + "/snapshot", {
|
|
51112
|
+
method: "POST",
|
|
51113
|
+
headers: { "content-type": "application/json" },
|
|
51114
|
+
body: JSON.stringify({
|
|
51115
|
+
meta: { v: 1, id: slug(location.pathname), name: document.title || location.pathname, url: location.href, runner: "dev" },
|
|
51116
|
+
dom: collected.dom, styles: collected.styles, boxes: collected.boxes, css: collected.css,
|
|
51117
|
+
}),
|
|
51118
|
+
});
|
|
51119
|
+
const json = await res.json();
|
|
51120
|
+
render(json.findings || [], json.error);
|
|
51121
|
+
} catch (e) {
|
|
51122
|
+
render([], "ultra11y dev server unreachable at " + ENDPOINT + " \u2014 run \`ultra11y dev\`.");
|
|
51123
|
+
}
|
|
51124
|
+
}
|
|
51125
|
+
|
|
51126
|
+
function slug(path) {
|
|
51127
|
+
const s = path.normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
51128
|
+
return s || (path === "/" ? "accueil" : "page");
|
|
51129
|
+
}
|
|
51130
|
+
|
|
51131
|
+
// Re-check on client-side navigation: patch the history methods a router uses, plus back/forward.
|
|
51132
|
+
let timer;
|
|
51133
|
+
const schedule = () => { clearTimeout(timer); timer = setTimeout(check, 400); };
|
|
51134
|
+
for (const m of ["pushState", "replaceState"]) {
|
|
51135
|
+
const orig = history[m];
|
|
51136
|
+
history[m] = function () { const r = orig.apply(this, arguments); schedule(); return r; };
|
|
51137
|
+
}
|
|
51138
|
+
addEventListener("popstate", schedule);
|
|
51139
|
+
schedule();
|
|
51140
|
+
})();`;
|
|
51141
|
+
}
|
|
51142
|
+
function nextOverlayComponent(port) {
|
|
51143
|
+
return `"use client";
|
|
51144
|
+
// ultra11y dev overlay for Next.js. Generated by \`ultra11y dev --next\`.
|
|
51145
|
+
//
|
|
51146
|
+
// // app/layout.tsx
|
|
51147
|
+
// import { Ultra11yOverlay } from "../.ultra11y/next/overlay";
|
|
51148
|
+
// \u2026
|
|
51149
|
+
// <body>{children}<Ultra11yOverlay /></body>
|
|
51150
|
+
//
|
|
51151
|
+
// Renders NOTHING in production: the whole component short-circuits on NODE_ENV, so shipping
|
|
51152
|
+
// it is inert. Start the side-car with \`ultra11y dev\` for it to have anything to talk to.
|
|
51153
|
+
import { useEffect } from "react";
|
|
51154
|
+
|
|
51155
|
+
export function Ultra11yOverlay({ endpoint = "http://127.0.0.1:${port}" }) {
|
|
51156
|
+
useEffect(() => {
|
|
51157
|
+
if (process.env.NODE_ENV !== "development") return;
|
|
51158
|
+
if (document.getElementById("__ultra11y_overlay_script")) return;
|
|
51159
|
+
window.__ULTRA11Y_ENDPOINT__ = endpoint;
|
|
51160
|
+
const s = document.createElement("script");
|
|
51161
|
+
s.id = "__ultra11y_overlay_script";
|
|
51162
|
+
s.src = endpoint + "/overlay.js";
|
|
51163
|
+
document.body.appendChild(s);
|
|
51164
|
+
}, [endpoint]);
|
|
51165
|
+
return null;
|
|
51166
|
+
}
|
|
51167
|
+
|
|
51168
|
+
export default Ultra11yOverlay;
|
|
51169
|
+
`;
|
|
51170
|
+
}
|
|
51171
|
+
var MARK2 = { C: "C", NC: "NC", NA: "\u2014", manual: "?" };
|
|
51172
|
+
var CLASS = { C: "c", NC: "nc", NA: "na", manual: "m" };
|
|
51173
|
+
function esc2(s) {
|
|
51174
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
51175
|
+
}
|
|
51176
|
+
function dashboardHtml(result, pages, standard, lang) {
|
|
51177
|
+
const fr = lang === "fr";
|
|
51178
|
+
const stdLabel = isCore(standard) ? "WCAG 2.2 AA" : loadPack(standard).name;
|
|
51179
|
+
const head = `<!doctype html><html lang="${fr ? "fr" : "en"}"><head><meta charset="utf-8">
|
|
51180
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
51181
|
+
<title>ultra11y \u2014 ${esc2(stdLabel)}</title>
|
|
51182
|
+
<style>
|
|
51183
|
+
:root { color-scheme: light dark; --bg:#fff; --fg:#111; --line:#e3e3e6; --mut:#666; }
|
|
51184
|
+
@media (prefers-color-scheme: dark) { :root { --bg:#141416; --fg:#f2f2f7; --line:#2c2c2e; --mut:#a1a1a6; } }
|
|
51185
|
+
body { margin:0; padding:24px; background:var(--bg); color:var(--fg); font:14px/1.5 ui-sans-serif,system-ui,sans-serif; }
|
|
51186
|
+
h1 { font-size:18px; margin:0 0 4px; } p.sub { color:var(--mut); margin:0 0 20px; }
|
|
51187
|
+
table { border-collapse:collapse; width:100%; } th,td { padding:5px 8px; border-bottom:1px solid var(--line); text-align:left; }
|
|
51188
|
+
th { position:sticky; top:0; background:var(--bg); }
|
|
51189
|
+
td.c,td.nc,td.na,td.m { text-align:center; font-weight:600; width:80px; }
|
|
51190
|
+
td.c{color:#0a7d33} td.nc{color:#b3261e} td.na{color:var(--mut)} td.m{color:#b06000}
|
|
51191
|
+
tr.theme td { font-weight:700; background:color-mix(in srgb, var(--fg) 6%, transparent); }
|
|
51192
|
+
.rate { font-size:22px; font-weight:700; }
|
|
51193
|
+
.empty { color:var(--mut); max-width:60ch; }
|
|
51194
|
+
code { font:12px ui-monospace,monospace; color:var(--mut); }
|
|
51195
|
+
</style></head><body>`;
|
|
51196
|
+
if (!result || !pages.length) {
|
|
51197
|
+
return `${head}<h1>ultra11y</h1>
|
|
51198
|
+
<p class="empty">${fr ? "Aucune page captur\xE9e pour l'instant. Ouvrez votre application avec l'overlay actif : chaque page visit\xE9e appara\xEEtra ici." : "No page captured yet. Open your app with the overlay active: every page you visit shows up here."}</p></body></html>`;
|
|
51199
|
+
}
|
|
51200
|
+
const rows = [];
|
|
51201
|
+
const status = /* @__PURE__ */ new Map();
|
|
51202
|
+
if (isCore(standard)) {
|
|
51203
|
+
for (const c2 of result.criteria) rows.push({ id: c2.id, label: c2.id, group: c2.guideline });
|
|
51204
|
+
for (const p of pages) for (const c2 of p.criteria) (status.get(c2.id) ?? status.set(c2.id, /* @__PURE__ */ new Map()).get(c2.id)).set(p.id, c2.status);
|
|
51205
|
+
} else {
|
|
51206
|
+
const pack = loadPack(standard);
|
|
51207
|
+
for (const pc of pack.criteria) rows.push({ id: pc.id, label: pc.id, group: `${pc.theme}. ${themeName(pack, pc.theme, lang) ?? ""}` });
|
|
51208
|
+
for (const p of pages) {
|
|
51209
|
+
const view = { ...result, criteria: p.criteria, findings: p.findings };
|
|
51210
|
+
for (const pc of derivePackResults(view, standard)) (status.get(pc.id) ?? status.set(pc.id, /* @__PURE__ */ new Map()).get(pc.id)).set(p.id, pc.status);
|
|
51211
|
+
}
|
|
51212
|
+
}
|
|
51213
|
+
const out2 = [head];
|
|
51214
|
+
out2.push(`<h1>ultra11y \u2014 ${esc2(stdLabel)}</h1>`);
|
|
51215
|
+
out2.push(`<p class="sub">${pages.length} ${fr ? "page(s) captur\xE9e(s)" : "page(s) captured"} \xB7 <code>${esc2(result.date)}</code></p>`);
|
|
51216
|
+
out2.push("<table><thead><tr><th></th>");
|
|
51217
|
+
for (const p of pages) out2.push(`<th>${esc2(p.name)}${p.auth ? " \u{1F512}" : ""}<br><code>${esc2(p.url)}</code></th>`);
|
|
51218
|
+
out2.push("</tr></thead><tbody>");
|
|
51219
|
+
out2.push(`<tr><td>${fr ? "Taux" : "Rate"}</td>${pages.map((p) => `<td class="rate">${p.conformancePct}%</td>`).join("")}</tr>`);
|
|
51220
|
+
let group = "";
|
|
51221
|
+
for (const row of rows) {
|
|
51222
|
+
if (row.group !== group) {
|
|
51223
|
+
group = row.group;
|
|
51224
|
+
out2.push(`<tr class="theme"><td colspan="${pages.length + 1}">${esc2(group)}</td></tr>`);
|
|
51225
|
+
}
|
|
51226
|
+
const cells = pages.map((p) => {
|
|
51227
|
+
const s = status.get(row.id)?.get(p.id) ?? "manual";
|
|
51228
|
+
return `<td class="${CLASS[s]}">${MARK2[s]}</td>`;
|
|
51229
|
+
});
|
|
51230
|
+
out2.push(`<tr><td>${esc2(row.label)}</td>${cells.join("")}</tr>`);
|
|
51231
|
+
}
|
|
51232
|
+
out2.push("</tbody></table></body></html>");
|
|
51233
|
+
return out2.join("\n");
|
|
51234
|
+
}
|
|
51235
|
+
function cors(res) {
|
|
51236
|
+
res.setHeader("access-control-allow-origin", "*");
|
|
51237
|
+
res.setHeader("access-control-allow-headers", "content-type");
|
|
51238
|
+
res.setHeader("access-control-allow-methods", "POST, GET, OPTIONS");
|
|
51239
|
+
}
|
|
51240
|
+
async function readBody(req, limit = 64 * 1024 * 1024) {
|
|
51241
|
+
const chunks = [];
|
|
51242
|
+
let size = 0;
|
|
51243
|
+
for await (const c2 of req) {
|
|
51244
|
+
const buf = c2;
|
|
51245
|
+
size += buf.length;
|
|
51246
|
+
if (size > limit) throw new Error("payload too large");
|
|
51247
|
+
chunks.push(buf);
|
|
51248
|
+
}
|
|
51249
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
51250
|
+
}
|
|
51251
|
+
function auditCollected(root, payload) {
|
|
51252
|
+
const v = validateSnapshotMeta(payload.meta);
|
|
51253
|
+
if (!v.ok || !v.meta) return { ok: false, error: v.issues.map((i2) => `${i2.path}: ${i2.message}`).join("; ") };
|
|
51254
|
+
if (typeof payload.dom !== "string" || !payload.dom.trim()) return { ok: false, error: "dom is required" };
|
|
51255
|
+
const dir = writeSnapshot(root, {
|
|
51256
|
+
meta: v.meta,
|
|
51257
|
+
dom: payload.dom,
|
|
51258
|
+
...payload.styles ? { styles: payload.styles } : {},
|
|
51259
|
+
...payload.boxes ? { boxes: payload.boxes } : {},
|
|
51260
|
+
...payload.axtree ? { axtree: payload.axtree } : {},
|
|
51261
|
+
...payload.css ? { css: payload.css } : {}
|
|
51262
|
+
});
|
|
51263
|
+
return { ok: true, result: runAudit({ inputs: [join32(dir, "dom.html")] }) };
|
|
51264
|
+
}
|
|
51265
|
+
function projectPages(root) {
|
|
51266
|
+
const snaps = readSnapshots(root);
|
|
51267
|
+
if (!snaps.length) return { result: null, pages: [] };
|
|
51268
|
+
const scope = pageScopesFrom(snaps);
|
|
51269
|
+
const result = runAudit({ inputs: [join32(root, ".ultra11y/pages")] });
|
|
51270
|
+
result.scope.pages = scope;
|
|
51271
|
+
attributePages(result, scope);
|
|
51272
|
+
return { result, pages: derivePages(result, scope) };
|
|
51273
|
+
}
|
|
51274
|
+
function startDevServer(opts) {
|
|
51275
|
+
const log = opts.onLog ?? (() => {
|
|
51276
|
+
});
|
|
51277
|
+
const server = createServer((req, res) => {
|
|
51278
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
51279
|
+
cors(res);
|
|
51280
|
+
if (req.method === "OPTIONS") {
|
|
51281
|
+
res.writeHead(204).end();
|
|
51282
|
+
return;
|
|
51283
|
+
}
|
|
51284
|
+
if (url.pathname === "/overlay.js") {
|
|
51285
|
+
res.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "cache-control": "no-store" }).end(overlayJs());
|
|
51286
|
+
return;
|
|
51287
|
+
}
|
|
51288
|
+
if (url.pathname === "/snapshot" && req.method === "POST") {
|
|
51289
|
+
void (async () => {
|
|
51290
|
+
try {
|
|
51291
|
+
const body2 = await readBody(req);
|
|
51292
|
+
const payload = JSON.parse(body2);
|
|
51293
|
+
const r = auditCollected(opts.root, payload);
|
|
51294
|
+
if (!r.ok) {
|
|
51295
|
+
res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: r.error }));
|
|
51296
|
+
return;
|
|
51297
|
+
}
|
|
51298
|
+
const nc = r.result.findings.filter((f) => !f.advisory);
|
|
51299
|
+
const name2 = payload.meta?.name ?? "page";
|
|
51300
|
+
log(opts.lang === "fr" ? `ultra11y dev : ${name2} \u2014 ${nc.length} non-conformit\xE9(s)` : `ultra11y dev: ${name2} \u2014 ${nc.length} non-conformity(ies)`);
|
|
51301
|
+
for (const f of nc.slice(0, 20)) {
|
|
51302
|
+
const where = f.origin?.sourceFile ?? f.file;
|
|
51303
|
+
log(
|
|
51304
|
+
` ${f.severity} ${criterionLabel3(f, opts.standard)} \xB7 ${where}:${Math.max(1, f.origin?.sourceLine ?? f.line)} \u2014 ${resolveMessage(f, opts.lang)}`
|
|
51305
|
+
);
|
|
51306
|
+
}
|
|
51307
|
+
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ findings: r.result.findings }));
|
|
51308
|
+
} catch (e) {
|
|
51309
|
+
res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
|
|
51310
|
+
}
|
|
51311
|
+
})();
|
|
51312
|
+
return;
|
|
51313
|
+
}
|
|
51314
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
51315
|
+
const { result, pages } = projectPages(opts.root);
|
|
51316
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }).end(dashboardHtml(result, pages, opts.standard, opts.lang));
|
|
51317
|
+
return;
|
|
51318
|
+
}
|
|
51319
|
+
res.writeHead(404, { "content-type": "text/plain" }).end("not found");
|
|
51320
|
+
});
|
|
51321
|
+
return new Promise((resolve10, reject) => {
|
|
51322
|
+
server.once("error", reject);
|
|
51323
|
+
server.listen(opts.port, "127.0.0.1", () => {
|
|
51324
|
+
resolve10({
|
|
51325
|
+
port: server.address().port,
|
|
51326
|
+
close: () => new Promise((r) => server.close(() => r()))
|
|
51327
|
+
});
|
|
51328
|
+
});
|
|
51329
|
+
});
|
|
51330
|
+
}
|
|
51331
|
+
|
|
51332
|
+
// src/e2e.ts
|
|
51333
|
+
function detectE2eRunner(deps, has2) {
|
|
51334
|
+
const dep = (n) => Object.hasOwn(deps, n);
|
|
51335
|
+
const out2 = [];
|
|
51336
|
+
if (dep("@playwright/test") || dep("playwright") || ["ts", "js", "mjs", "cjs"].some((e) => has2(`playwright.config.${e}`))) out2.push("playwright");
|
|
51337
|
+
if (dep("cypress") || ["ts", "js", "mjs", "cjs"].some((e) => has2(`cypress.config.${e}`))) out2.push("cypress");
|
|
51338
|
+
return out2;
|
|
51339
|
+
}
|
|
51340
|
+
function runnerCore(enginePath) {
|
|
51341
|
+
return `import { spawnSync } from "node:child_process";
|
|
51342
|
+
import { createRequire } from "node:module";
|
|
51343
|
+
|
|
51344
|
+
// Engine resolution, in order: the ULTRA11Y env override (same convention as the git hook),
|
|
51345
|
+
// then the path baked in when \`render --e2e\` generated this file.
|
|
51346
|
+
const ENGINE = process.env.ULTRA11Y || ${JSON.stringify(enginePath)};
|
|
51347
|
+
|
|
51348
|
+
/** Persist a collected page as a snapshot and audit it. Returns the AuditResult. */
|
|
51349
|
+
export function auditSnapshot(payload) {
|
|
51350
|
+
const res = spawnSync(process.execPath, [ENGINE, "snapshot", "write", "--json"], {
|
|
51351
|
+
input: JSON.stringify(payload),
|
|
51352
|
+
encoding: "utf8",
|
|
51353
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
51354
|
+
});
|
|
51355
|
+
if (res.error) throw new Error("ultra11y: could not run the engine at " + ENGINE + " \u2014 " + res.error.message);
|
|
51356
|
+
if (!res.stdout) throw new Error("ultra11y: the engine produced no output (exit " + res.status + ")\\n" + (res.stderr || ""));
|
|
51357
|
+
try {
|
|
51358
|
+
return JSON.parse(res.stdout);
|
|
51359
|
+
} catch {
|
|
51360
|
+
throw new Error("ultra11y: could not parse the engine output\\n" + res.stdout.slice(0, 500));
|
|
51361
|
+
}
|
|
51362
|
+
}
|
|
51363
|
+
|
|
51364
|
+
const RANK = { bloquant: 0, majeur: 1, mineur: 2 };
|
|
51365
|
+
const THRESHOLD = { blocking: 0, bloquant: 0, major: 1, majeur: 1, minor: 2, mineur: 2 };
|
|
51366
|
+
|
|
51367
|
+
/** Findings at or above the threshold, ignoring non-normative recommendations. */
|
|
51368
|
+
export function failingFindings(result, failOn) {
|
|
51369
|
+
const max = THRESHOLD[failOn];
|
|
51370
|
+
if (max === undefined) throw new Error('ultra11y: failOn must be blocking|major|minor (got "' + failOn + '")');
|
|
51371
|
+
return (result.findings || []).filter((f) => !f.advisory && RANK[f.severity] <= max);
|
|
51372
|
+
}
|
|
51373
|
+
|
|
51374
|
+
export function formatFailure(pageName, failing) {
|
|
51375
|
+
const lines = ["ultra11y: " + failing.length + " accessibility non-conformity(ies) on \\"" + pageName + "\\":"];
|
|
51376
|
+
for (const f of failing.slice(0, 20)) {
|
|
51377
|
+
lines.push(" [" + f.severity + "] " + f.ruleId + " (WCAG " + f.criteriaId + ") \u2014 " + (f.origin && f.origin.sourceFile ? f.origin.sourceFile : f.file) + " \u2014 " + f.message);
|
|
51378
|
+
}
|
|
51379
|
+
if (failing.length > 20) lines.push(" \u2026 and " + (failing.length - 20) + " more.");
|
|
51380
|
+
lines.push("Full detail: .ultra11y/pages/ \u2014 re-audit offline with \`ultra11y audit\`.");
|
|
51381
|
+
return lines.join("\\n");
|
|
51382
|
+
}
|
|
51383
|
+
`;
|
|
51384
|
+
}
|
|
51385
|
+
function playwrightFixture(enginePath) {
|
|
51386
|
+
return `// ultra11y \u2014 Playwright integration. Generated by \`ultra11y render --e2e\`.
|
|
51387
|
+
//
|
|
51388
|
+
// import { test, checkA11y } from "../.ultra11y/e2e/playwright.mjs";
|
|
51389
|
+
//
|
|
51390
|
+
// test("home page is accessible", async ({ page, ultra11y }) => {
|
|
51391
|
+
// await page.goto("/");
|
|
51392
|
+
// await ultra11y({ as: "accueil" }); // via the fixture
|
|
51393
|
+
// });
|
|
51394
|
+
//
|
|
51395
|
+
// // \u2026or without the fixture:
|
|
51396
|
+
// await checkA11y(page, { as: "accueil", failOn: "blocking" });
|
|
51397
|
+
//
|
|
51398
|
+
// Each checked page is persisted to .ultra11y/pages/<id>/ (DOM + computed styles + boxes),
|
|
51399
|
+
// so the SAME page can be re-audited later, offline, with no browser \u2014 that is what lets CI
|
|
51400
|
+
// and the RGAA report speak page by page. Commit the snapshots to gate on them.
|
|
51401
|
+
//
|
|
51402
|
+
// Options: { as?: string (page id), name?: string, failOn?: "blocking"|"major"|"minor"|false,
|
|
51403
|
+
// auth?: boolean, sources?: string[], notes?: string }
|
|
51404
|
+
// \`failOn: false\` records the snapshot without ever failing the test.
|
|
51405
|
+
${runnerCore(enginePath)}
|
|
51406
|
+
const COLLECT = ${JSON.stringify(COLLECT_SNAPSHOT)};
|
|
51407
|
+
|
|
51408
|
+
export async function checkA11y(page, opts = {}) {
|
|
51409
|
+
const collected = await page.evaluate(COLLECT);
|
|
51410
|
+
// A VIEWPORT screenshot, deliberately \u2014 the boxes come from getBoundingClientRect, which is
|
|
51411
|
+
// viewport-relative, so a full-page capture would put the two coordinate systems out of
|
|
51412
|
+
// step. It feeds the pixel tier: contrast where the CSSOM cannot answer (text over an
|
|
51413
|
+
// image or a gradient). \`screenshot: false\` skips it.
|
|
51414
|
+
let shot;
|
|
51415
|
+
if (opts.screenshot !== false) {
|
|
51416
|
+
try {
|
|
51417
|
+
shot = (await page.screenshot({ fullPage: false })).toString("base64");
|
|
51418
|
+
} catch {
|
|
51419
|
+
// A screenshot failure must never fail the accessibility check itself.
|
|
51420
|
+
}
|
|
51421
|
+
}
|
|
51422
|
+
const url = collected.url || page.url();
|
|
51423
|
+
const id = opts.as || slugify(url);
|
|
51424
|
+
const payload = {
|
|
51425
|
+
meta: {
|
|
51426
|
+
v: 1,
|
|
51427
|
+
id: id,
|
|
51428
|
+
name: opts.name || collected.title || id,
|
|
51429
|
+
url: url,
|
|
51430
|
+
runner: "playwright",
|
|
51431
|
+
viewport: collected.viewport,
|
|
51432
|
+
capturedAt: new Date().toISOString(),
|
|
51433
|
+
...(opts.auth !== undefined ? { auth: opts.auth } : {}),
|
|
51434
|
+
...(opts.sources ? { sources: opts.sources } : {}),
|
|
51435
|
+
...(opts.notes ? { notes: opts.notes } : {}),
|
|
51436
|
+
},
|
|
51437
|
+
dom: collected.dom,
|
|
51438
|
+
styles: collected.styles,
|
|
51439
|
+
boxes: collected.boxes,
|
|
51440
|
+
css: collected.css,
|
|
51441
|
+
...(shot ? { screenshot: shot } : {}),
|
|
51442
|
+
};
|
|
51443
|
+
const result = auditSnapshot(payload);
|
|
51444
|
+
const failOn = opts.failOn === undefined ? "blocking" : opts.failOn;
|
|
51445
|
+
if (failOn !== false) {
|
|
51446
|
+
const failing = failingFindings(result, failOn);
|
|
51447
|
+
if (failing.length) throw new Error(formatFailure(payload.meta.name, failing));
|
|
51448
|
+
}
|
|
51449
|
+
return result;
|
|
51450
|
+
}
|
|
51451
|
+
|
|
51452
|
+
// Same slug rule as the engine (src/snapshot.ts slugifyPageId): accent-folded URL path.
|
|
51453
|
+
function slugify(url) {
|
|
51454
|
+
let path = url;
|
|
51455
|
+
try {
|
|
51456
|
+
path = new URL(url).pathname;
|
|
51457
|
+
} catch {}
|
|
51458
|
+
const slug = path.normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
51459
|
+
return slug || (path === "/" || path === "" ? "accueil" : "page");
|
|
51460
|
+
}
|
|
51461
|
+
|
|
51462
|
+
// Optional fixture form. Import \`test\` from here instead of @playwright/test.
|
|
51463
|
+
// Playwright is resolved through createRequire (this file is ESM, so \`require\` does not
|
|
51464
|
+
// exist) and lazily, so the module still loads in a project that has not installed it.
|
|
51465
|
+
export const test = (() => {
|
|
51466
|
+
try {
|
|
51467
|
+
const base = createRequire(import.meta.url)("@playwright/test").test;
|
|
51468
|
+
return base.extend({
|
|
51469
|
+
ultra11y: async ({ page }, use) => {
|
|
51470
|
+
await use((opts) => checkA11y(page, opts));
|
|
51471
|
+
},
|
|
51472
|
+
});
|
|
51473
|
+
} catch {
|
|
51474
|
+
return undefined;
|
|
51475
|
+
}
|
|
51476
|
+
})();
|
|
51477
|
+
`;
|
|
51478
|
+
}
|
|
51479
|
+
function cypressPlugin(enginePath) {
|
|
51480
|
+
return `// ultra11y \u2014 Cypress plugin (Node side). Generated by \`ultra11y render --e2e\`.
|
|
51481
|
+
//
|
|
51482
|
+
// // cypress.config.js
|
|
51483
|
+
// import ultra11y from "./.ultra11y/e2e/cypress-plugin.mjs";
|
|
51484
|
+
// export default defineConfig({ e2e: { setupNodeEvents(on, config) { ultra11y(on); return config; } } });
|
|
51485
|
+
//
|
|
51486
|
+
// Cypress test code runs in the BROWSER and cannot write to disk, so the collected page
|
|
51487
|
+
// round-trips through this task. The browser half is cypress-commands.mjs.
|
|
51488
|
+
${runnerCore(enginePath)}
|
|
51489
|
+
export default function register(on) {
|
|
51490
|
+
on("task", {
|
|
51491
|
+
ultra11ySnapshot(payload) {
|
|
51492
|
+
const result = auditSnapshot(payload);
|
|
51493
|
+
const failOn = payload.failOn === undefined ? "blocking" : payload.failOn;
|
|
51494
|
+
const failing = failOn === false ? [] : failingFindings(result, failOn);
|
|
51495
|
+
// Return rather than throw: the browser half raises the assertion, so the failure is
|
|
51496
|
+
// attributed to the test rather than to the plugin.
|
|
51497
|
+
return { findings: result.findings || [], failing: failing, message: failing.length ? formatFailure(payload.meta.name, failing) : "" };
|
|
51498
|
+
},
|
|
51499
|
+
});
|
|
51500
|
+
}
|
|
51501
|
+
`;
|
|
51502
|
+
}
|
|
51503
|
+
function cypressCommands() {
|
|
51504
|
+
return `// ultra11y \u2014 Cypress command (browser side). Generated by \`ultra11y render --e2e\`.
|
|
51505
|
+
//
|
|
51506
|
+
// // cypress/support/e2e.js (the supportFile)
|
|
51507
|
+
// import "../../.ultra11y/e2e/cypress-commands.mjs";
|
|
51508
|
+
//
|
|
51509
|
+
// cy.visit("/");
|
|
51510
|
+
// cy.ultra11y({ as: "accueil" });
|
|
51511
|
+
//
|
|
51512
|
+
// Options: { as?, name?, failOn?: "blocking"|"major"|"minor"|false, auth?, sources?, notes? }
|
|
51513
|
+
const COLLECT = ${JSON.stringify(COLLECT_SNAPSHOT)};
|
|
51514
|
+
|
|
51515
|
+
Cypress.Commands.add("ultra11y", (opts = {}) => {
|
|
51516
|
+
cy.window({ log: false }).then((win) => {
|
|
51517
|
+
// eslint-disable-next-line no-eval
|
|
51518
|
+
const collected = win.eval(COLLECT);
|
|
51519
|
+
const url = collected.url || win.location.href;
|
|
51520
|
+
const id = opts.as || slugify(url);
|
|
51521
|
+
const payload = {
|
|
51522
|
+
meta: {
|
|
51523
|
+
v: 1,
|
|
51524
|
+
id: id,
|
|
51525
|
+
name: opts.name || collected.title || id,
|
|
51526
|
+
url: url,
|
|
51527
|
+
runner: "cypress",
|
|
51528
|
+
viewport: collected.viewport,
|
|
51529
|
+
capturedAt: new Date().toISOString(),
|
|
51530
|
+
...(opts.auth !== undefined ? { auth: opts.auth } : {}),
|
|
51531
|
+
...(opts.sources ? { sources: opts.sources } : {}),
|
|
51532
|
+
...(opts.notes ? { notes: opts.notes } : {}),
|
|
51533
|
+
},
|
|
51534
|
+
dom: collected.dom,
|
|
51535
|
+
styles: collected.styles,
|
|
51536
|
+
boxes: collected.boxes,
|
|
51537
|
+
css: collected.css,
|
|
51538
|
+
failOn: opts.failOn,
|
|
51539
|
+
};
|
|
51540
|
+
return cy.task("ultra11ySnapshot", payload, { log: false }).then((res) => {
|
|
51541
|
+
if (res && res.failing && res.failing.length) throw new Error(res.message);
|
|
51542
|
+
return res;
|
|
51543
|
+
});
|
|
51544
|
+
});
|
|
51545
|
+
});
|
|
51546
|
+
|
|
51547
|
+
function slugify(url) {
|
|
51548
|
+
let path = url;
|
|
51549
|
+
try {
|
|
51550
|
+
path = new URL(url).pathname;
|
|
51551
|
+
} catch (e) {}
|
|
51552
|
+
const slug = path.normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
51553
|
+
return slug || (path === "/" || path === "" ? "accueil" : "page");
|
|
51554
|
+
}
|
|
51555
|
+
`;
|
|
51556
|
+
}
|
|
51557
|
+
function e2eSetupPlan(runners, paths, lang = "en") {
|
|
51558
|
+
const fr = lang === "fr";
|
|
51559
|
+
const out2 = [];
|
|
51560
|
+
if (!runners.length) {
|
|
51561
|
+
out2.push(
|
|
51562
|
+
fr ? "Aucun runner E2E d\xE9tect\xE9 (ni Playwright, ni Cypress). Installez-en un, ou forcez l'\xE9criture avec --runner playwright|cypress." : "No E2E runner detected (neither Playwright nor Cypress). Install one, or force the files with --runner playwright|cypress."
|
|
51563
|
+
);
|
|
51564
|
+
return out2.join("\n");
|
|
51565
|
+
}
|
|
51566
|
+
if (paths.playwright) {
|
|
51567
|
+
out2.push(fr ? `Fixture Playwright \xE9crite : ${paths.playwright}` : `Playwright fixture written: ${paths.playwright}`);
|
|
51568
|
+
out2.push("");
|
|
51569
|
+
out2.push(fr ? "Dans un test :" : "In a test:");
|
|
51570
|
+
out2.push(` import { test, checkA11y } from "${paths.playwright}";`);
|
|
51571
|
+
out2.push(` await page.goto("/");`);
|
|
51572
|
+
out2.push(` await checkA11y(page, { as: "accueil" });`);
|
|
51573
|
+
out2.push("");
|
|
51574
|
+
}
|
|
51575
|
+
if (paths.cypressPlugin) {
|
|
51576
|
+
out2.push(fr ? `Plugin Cypress \xE9crit : ${paths.cypressPlugin}` : `Cypress plugin written: ${paths.cypressPlugin}`);
|
|
51577
|
+
if (paths.cypressCommands) out2.push(fr ? `Commande Cypress \xE9crite : ${paths.cypressCommands}` : `Cypress command written: ${paths.cypressCommands}`);
|
|
51578
|
+
out2.push("");
|
|
51579
|
+
out2.push(fr ? "Dans cypress.config.js (setupNodeEvents) :" : "In cypress.config.js (setupNodeEvents):");
|
|
51580
|
+
out2.push(` import ultra11y from "./${paths.cypressPlugin}";`);
|
|
51581
|
+
out2.push(` setupNodeEvents(on, config) { ultra11y(on); return config; }`);
|
|
51582
|
+
out2.push(fr ? "Puis dans le supportFile :" : "Then in the supportFile:");
|
|
51583
|
+
out2.push(` import "${paths.cypressCommands ? `../../${paths.cypressCommands}` : ""}";`);
|
|
51584
|
+
out2.push(fr ? "Dans un test : cy.visit('/'); cy.ultra11y({ as: 'accueil' });" : "In a test: cy.visit('/'); cy.ultra11y({ as: 'accueil' });");
|
|
51585
|
+
out2.push("");
|
|
51586
|
+
}
|
|
51587
|
+
out2.push(
|
|
51588
|
+
fr ? "Chaque page v\xE9rifi\xE9e est enregistr\xE9e dans .ultra11y/pages/<id>/ \u2014 committez-la pour auditer la vraie page hors ligne (CI, rapport page par page)." : "Every checked page is recorded in .ultra11y/pages/<id>/ \u2014 commit it to audit the real page offline (CI, per-page report)."
|
|
51589
|
+
);
|
|
51590
|
+
return out2.join("\n");
|
|
51591
|
+
}
|
|
51592
|
+
|
|
49383
51593
|
// src/config.ts
|
|
49384
|
-
import { existsSync as
|
|
49385
|
-
import { join as
|
|
51594
|
+
import { existsSync as existsSync18, statSync as statSync9 } from "fs";
|
|
51595
|
+
import { join as join33, isAbsolute as isAbsolute2 } from "path";
|
|
49386
51596
|
|
|
49387
51597
|
// src/pack.ts
|
|
49388
|
-
import { existsSync as
|
|
51598
|
+
import { existsSync as existsSync17 } from "fs";
|
|
49389
51599
|
function exampleParses(ex) {
|
|
49390
51600
|
for (const code2 of [ex.bad, ex.good]) {
|
|
49391
51601
|
if (!code2) continue;
|
|
@@ -49456,7 +51666,7 @@ function checkGuidance(ds, pack) {
|
|
|
49456
51666
|
function runPackCheck(packPath, guidancePath) {
|
|
49457
51667
|
const errors = [];
|
|
49458
51668
|
const warnings = [];
|
|
49459
|
-
if (!
|
|
51669
|
+
if (!existsSync17(packPath)) return { ok: false, errors: [`pack file not found: ${packPath}`], warnings };
|
|
49460
51670
|
let raw;
|
|
49461
51671
|
try {
|
|
49462
51672
|
raw = JSON.parse(readText(packPath));
|
|
@@ -49480,7 +51690,7 @@ function runPackCheck(packPath, guidancePath) {
|
|
|
49480
51690
|
}
|
|
49481
51691
|
}
|
|
49482
51692
|
if (v.ok && v.pack && guidancePath) {
|
|
49483
|
-
if (!
|
|
51693
|
+
if (!existsSync17(guidancePath)) {
|
|
49484
51694
|
errors.push(`guidance file not found: ${guidancePath}`);
|
|
49485
51695
|
} else {
|
|
49486
51696
|
let gRaw;
|
|
@@ -49534,8 +51744,8 @@ function packScaffold() {
|
|
|
49534
51744
|
// src/config.ts
|
|
49535
51745
|
var CONFIG_FILE = ".ultra11yrc.json";
|
|
49536
51746
|
function loadConfig(cwd) {
|
|
49537
|
-
const p =
|
|
49538
|
-
if (!
|
|
51747
|
+
const p = join33(cwd, CONFIG_FILE);
|
|
51748
|
+
if (!existsSync18(p)) return null;
|
|
49539
51749
|
let parsed;
|
|
49540
51750
|
try {
|
|
49541
51751
|
parsed = JSON.parse(readText(p));
|
|
@@ -49548,16 +51758,16 @@ function loadConfig(cwd) {
|
|
|
49548
51758
|
return parsed;
|
|
49549
51759
|
}
|
|
49550
51760
|
function packPaths(path) {
|
|
49551
|
-
if (!
|
|
51761
|
+
if (!existsSync18(path)) return null;
|
|
49552
51762
|
if (statSync9(path).isDirectory()) {
|
|
49553
|
-
const pack =
|
|
49554
|
-
if (!
|
|
49555
|
-
const glossary =
|
|
49556
|
-
const guidance =
|
|
51763
|
+
const pack = join33(path, "pack.json");
|
|
51764
|
+
if (!existsSync18(pack)) return null;
|
|
51765
|
+
const glossary = join33(path, "glossary.json");
|
|
51766
|
+
const guidance = join33(path, "guidance.json");
|
|
49557
51767
|
return {
|
|
49558
51768
|
pack,
|
|
49559
|
-
glossary:
|
|
49560
|
-
guidance:
|
|
51769
|
+
glossary: existsSync18(glossary) ? glossary : void 0,
|
|
51770
|
+
guidance: existsSync18(guidance) ? guidance : void 0
|
|
49561
51771
|
};
|
|
49562
51772
|
}
|
|
49563
51773
|
return { pack: path };
|
|
@@ -49573,7 +51783,7 @@ function loadRuntimeStandards(cwd, packFlags, onWarn, override = false) {
|
|
|
49573
51783
|
}
|
|
49574
51784
|
result.defaultStandard = config?.standard;
|
|
49575
51785
|
for (const raw of [...config?.packs ?? [], ...packFlags]) {
|
|
49576
|
-
const paths = packPaths(isAbsolute2(raw) ? raw :
|
|
51786
|
+
const paths = packPaths(isAbsolute2(raw) ? raw : join33(cwd, raw));
|
|
49577
51787
|
if (!paths) {
|
|
49578
51788
|
result.errors.push(`--pack ${raw}: not found (expected a pack JSON file or a directory with pack.json)`);
|
|
49579
51789
|
continue;
|
|
@@ -49603,7 +51813,7 @@ ${formatIssues(v.issues).join("\n")}`);
|
|
|
49603
51813
|
result.loadedPacks.push(v.pack.key);
|
|
49604
51814
|
if (paths.guidance) loadGuidanceFile(paths.guidance, result, onWarn);
|
|
49605
51815
|
}
|
|
49606
|
-
for (const g of config?.guidance ?? []) loadGuidanceFile(isAbsolute2(g) ? g :
|
|
51816
|
+
for (const g of config?.guidance ?? []) loadGuidanceFile(isAbsolute2(g) ? g : join33(cwd, g), result, onWarn);
|
|
49607
51817
|
for (const sm of config?.secondaryMappings ?? []) {
|
|
49608
51818
|
if (!sm || typeof sm.standard !== "string" || typeof sm.ruleId !== "string" || typeof sm.criterion !== "string") {
|
|
49609
51819
|
result.errors.push("secondaryMappings: each entry must be { standard, ruleId, criterion, note? }");
|
|
@@ -49618,7 +51828,7 @@ ${formatIssues(v.issues).join("\n")}`);
|
|
|
49618
51828
|
return result;
|
|
49619
51829
|
}
|
|
49620
51830
|
function loadGuidanceFile(path, result, onWarn) {
|
|
49621
|
-
if (!
|
|
51831
|
+
if (!existsSync18(path)) {
|
|
49622
51832
|
onWarn(`ultra11y: guidance ${path} not found \u2014 skipping.`);
|
|
49623
51833
|
return;
|
|
49624
51834
|
}
|
|
@@ -49656,11 +51866,11 @@ ${g.errors.map((e) => ` \u2717 ${e}`).join("\n")}`);
|
|
|
49656
51866
|
}
|
|
49657
51867
|
|
|
49658
51868
|
// src/orchestrate.ts
|
|
49659
|
-
import { existsSync as
|
|
49660
|
-
import { join as
|
|
51869
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync16, rmSync as rmSync4, writeFileSync as writeFileSync13 } from "fs";
|
|
51870
|
+
import { join as join35, resolve as resolve7 } from "path";
|
|
49661
51871
|
|
|
49662
51872
|
// src/orchestrate-templates.ts
|
|
49663
|
-
import { join as
|
|
51873
|
+
import { join as join34 } from "path";
|
|
49664
51874
|
var ONE_WRITER_FOOTER = `
|
|
49665
51875
|
## Return, don't write
|
|
49666
51876
|
|
|
@@ -49685,14 +51895,21 @@ var ADJUDICATE_SCHEMA = {
|
|
|
49685
51895
|
description: "REQUIRED (>=1, groundable) for NC",
|
|
49686
51896
|
items: {
|
|
49687
51897
|
type: "object",
|
|
49688
|
-
|
|
51898
|
+
// `normativeRef` is REQUIRED by the fold (src/adjudicate.ts): an NC with none is
|
|
51899
|
+
// rejected. Omitting it here made every fan-out adjudication produce NC verdicts
|
|
51900
|
+
// that `verify --apply` then refused.
|
|
51901
|
+
required: ["file", "line", "message", "normativeRef"],
|
|
49689
51902
|
properties: {
|
|
49690
51903
|
file: { type: "string" },
|
|
49691
51904
|
line: { type: "integer" },
|
|
49692
51905
|
selector: { type: "string" },
|
|
49693
51906
|
message: { type: "string" },
|
|
49694
51907
|
snippet: { type: "string" },
|
|
49695
|
-
severity: { enum: ["bloquant", "majeur", "mineur"] }
|
|
51908
|
+
severity: { enum: ["bloquant", "majeur", "mineur"] },
|
|
51909
|
+
normativeRef: {
|
|
51910
|
+
type: "string",
|
|
51911
|
+
description: "The precise failed test of the ACTIVE standard. WCAG core: a success-criterion id (e.g. '1.1.1'). Country standard: a test OF THIS ITEM'S OWN CRITERION (e.g. '11.2.1' on item 11.2) \u2014 a WCAG id looks alike but denotes an unrelated test and is rejected."
|
|
51912
|
+
}
|
|
49696
51913
|
}
|
|
49697
51914
|
}
|
|
49698
51915
|
}
|
|
@@ -49725,7 +51942,7 @@ var PHASE_SPECS = {
|
|
|
49725
51942
|
title: "Adjudicate",
|
|
49726
51943
|
schema: ADJUDICATE_SCHEMA,
|
|
49727
51944
|
description: (n) => `Adjudicate the ${n} residual judgment criterion(ia) of an ultra11y audit (fan-out, fail-closed fold)`,
|
|
49728
|
-
applyHint: (engine, worklist, run2) => `node ${engine} verify --apply ${worklist} --in ${
|
|
51945
|
+
applyHint: (engine, worklist, run2) => `node ${engine} verify --apply ${worklist} --in ${join34(run2, "audit-latest.json")} --out ${run2}`
|
|
49729
51946
|
},
|
|
49730
51947
|
"verify-report": {
|
|
49731
51948
|
role: "refuter",
|
|
@@ -49747,7 +51964,7 @@ function toBatches(ids, batchSize) {
|
|
|
49747
51964
|
}
|
|
49748
51965
|
function phaseWorkflowScript(ph, runAbs, engineAbs, batchSize) {
|
|
49749
51966
|
const spec = phaseSpec(ph.name);
|
|
49750
|
-
const scriptPath =
|
|
51967
|
+
const scriptPath = join34(runAbs, "orchestration", `${ph.name}.workflow.mjs`);
|
|
49751
51968
|
const meta2 = { name: `ultra11y-${ph.name}`, description: spec.description(ph.items), phases: [{ title: spec.title }] };
|
|
49752
51969
|
return [
|
|
49753
51970
|
`export const meta = ${JSON.stringify(meta2)}`,
|
|
@@ -49789,27 +52006,27 @@ function agentContracts(runAbs, engineAbs) {
|
|
|
49789
52006
|
return {
|
|
49790
52007
|
adjudicator: `# Contract: adjudicator
|
|
49791
52008
|
|
|
49792
|
-
You adjudicate residual judgment criteria of an ultra11y
|
|
52009
|
+
You adjudicate the residual judgment criteria of an ultra11y audit \u2014 the ones the deterministic engine could not decide (alt-text relevance, link purpose in context, reading order\u2026). The ACTIVE STANDARD is recorded in the worklist's \`standard\` field: under a country standard (e.g. \`rgaa\`) the items are that standard's OWN criteria, each carrying its numbered tests \u2014 not WCAG success criteria.
|
|
49793
52010
|
|
|
49794
|
-
Worklist: \`${
|
|
52011
|
+
Worklist: \`${join34(runAbs, "ADJUDICATE.todo.json")}\` (an object with \`kind: "adjudication"\` and \`items[]\`). Handle ONLY the criteria whose \`criteriaId\` is named in your prompt (\`ITEMS=<id,\u2026>\`).
|
|
49795
52012
|
|
|
49796
52013
|
For EACH of your criteria:
|
|
49797
52014
|
|
|
49798
52015
|
1. Read its worklist entry. \`evidence[]\` holds source-anchored excerpts (\`file\`, \`line\`, \`selector\`, \`snippet\`) harvested from the audited code \u2014 open the cited files at the cited lines whenever the snippet alone cannot decide.
|
|
49799
52016
|
2. Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict missing its required field does not fold):
|
|
49800
52017
|
- \`C\` (conforming) \u2014 REQUIRES \`justification\` explaining why the evidence satisfies the criterion.
|
|
49801
|
-
- \`NC\` (non-conforming) \u2014 REQUIRES \`findings\`: at least one groundable \`{ file, line, selector?, message, snippet?, severity
|
|
52018
|
+
- \`NC\` (non-conforming) \u2014 REQUIRES \`findings\`: at least one groundable \`{ file, line, selector?, message, snippet?, severity?, normativeRef }\` pointing at REAL source. The fold re-grounds every finding; an invented file:line is rejected. \`normativeRef\` MUST cite the precise failed test \u2014 under a country standard, one of the item's OWN tests, which the worklist lists for you under \xAB tests to rule on \xBB.
|
|
49802
52019
|
- \`NA\` (not applicable) \u2014 REQUIRES \`justification\`.
|
|
49803
52020
|
- \`manual\` (still undecidable) \u2014 REQUIRES \`reason\`: \`needs-rendered-dom\` (only a rendered DOM can decide, e.g. computed contrast) or \`undecidable\` (the evidence cannot settle it either way).
|
|
49804
52021
|
3. Never guess. A criterion you cannot decide from real evidence stays \`manual\` with a reason \u2014 that is a valid, honest verdict; the scan tier or a human picks it up.
|
|
49805
52022
|
|
|
49806
|
-
Return (structured output): \`{ "verdicts": [{ "criteriaId", "verdict", "justification", "reason", "findings" }] }\` \u2014 your ITEMS only, every field grounded in what you actually read
|
|
52023
|
+
Return (structured output): \`{ "verdicts": [{ "criteriaId", "verdict", "justification", "reason", "findings" }] }\` \u2014 your ITEMS only, every field grounded in what you actually read, every NC finding carrying its \`normativeRef\`.
|
|
49807
52024
|
${footer}`,
|
|
49808
52025
|
refuter: `# Contract: refuter
|
|
49809
52026
|
|
|
49810
52027
|
You are an adversarial skeptic verifying the non-conformities of an ultra11y report. Your job is to try to REFUTE each claim: assume it is wrong until the source proves it.
|
|
49811
52028
|
|
|
49812
|
-
Worklist: \`${
|
|
52029
|
+
Worklist: \`${join34(runAbs, "VERIFY.todo.json")}\` (a JSON array; each entry has \`n\`, \`criteriaId\`, \`file\`, \`line\`, \`selector\`, \`claim\`). Handle ONLY the entries whose \`n\` is named in your prompt (\`ITEMS=<n,\u2026>\`).
|
|
49813
52030
|
|
|
49814
52031
|
For EACH of your entries:
|
|
49815
52032
|
|
|
@@ -49845,14 +52062,14 @@ ${status}
|
|
|
49845
52062
|
|
|
49846
52063
|
## The loop (play every role yourself, one item at a time)
|
|
49847
52064
|
|
|
49848
|
-
1. **Audit** (if not done): \`${engine} audit "<globs>" --graph --out ${runAbs}\` \u2192 \`${
|
|
49849
|
-
2. **Adjudicate the residual criteria** \u2014 \`${engine} verify --manual --in ${
|
|
49850
|
-
3. **Report**: \`${engine} report --in ${
|
|
49851
|
-
4. **Verify the report's claims** \u2014 \`${engine} verify --report <the report .md> --out ${runAbs}\` writes \`${
|
|
52065
|
+
1. **Audit** (if not done): \`${engine} audit "<globs>" --graph --out ${runAbs}\` \u2192 \`${join34(runAbs, "audit-latest.json")}\`.
|
|
52066
|
+
2. **Adjudicate the residual criteria** \u2014 \`${engine} verify --manual --in ${join34(runAbs, "audit-latest.json")} --out ${runAbs}\` writes \`${join34(runAbs, "ADJUDICATE.todo.json")}\`. For EVERY item, apply \`${join34(runAbs, "orchestration", "agents", "adjudicator.md")}\` yourself (read the evidence, rule C/NC/NA/manual, fill the required justification/findings/reason IN the todo file). Then fold, fail-closed: \`${engine} verify --apply ${join34(runAbs, "ADJUDICATE.todo.json")} --in ${join34(runAbs, "audit-latest.json")} --out ${runAbs}\`.
|
|
52067
|
+
3. **Report**: \`${engine} report --in ${join34(runAbs, "audit-latest.json")} --out ${runAbs}\`.
|
|
52068
|
+
4. **Verify the report's claims** \u2014 \`${engine} verify --report <the report .md> --out ${runAbs}\` writes \`${join34(runAbs, "VERIFY.todo.json")}\`. For EVERY entry, apply \`${join34(runAbs, "orchestration", "agents", "refuter.md")}\` yourself (open file:line, verdict supported/partial/refuted/unsupported + note IN the todo file). Then: \`${engine} verify --apply ${join34(runAbs, "VERIFY.todo.json")} --report <the report .md>\`.
|
|
49852
52069
|
5. **Gate**: \`${engine} check --report <the report .md> --semantic\` must exit 0 before presenting anything.
|
|
49853
52070
|
6. **Fix & re-audit**: \`${engine} fix <globs> --write --iterate\`, hand-apply the judgment fixes, then loop from step 1 until the gate stays green.
|
|
49854
52071
|
|
|
49855
|
-
With subagents available, prefer the emitted workflows instead: \`orchestrate --run ${runAbs} --phase <p>\` then \`Workflow({ scriptPath: "${
|
|
52072
|
+
With subagents available, prefer the emitted workflows instead: \`orchestrate --run ${runAbs} --phase <p>\` then \`Workflow({ scriptPath: "${join34(runAbs, "orchestration", "<p>.workflow.mjs")}" })\` \u2014 you stay the sole writer either way.
|
|
49856
52073
|
`;
|
|
49857
52074
|
}
|
|
49858
52075
|
|
|
@@ -49862,12 +52079,12 @@ var SMALL_WORKLIST = 3;
|
|
|
49862
52079
|
var BATCH_SIZE = 8;
|
|
49863
52080
|
function listPhases(runDir, engineAbs) {
|
|
49864
52081
|
const run2 = resolve7(runDir);
|
|
49865
|
-
const adjPath =
|
|
52082
|
+
const adjPath = join35(run2, "ADJUDICATE.todo.json");
|
|
49866
52083
|
let adjIds = [];
|
|
49867
52084
|
let adjReady = false;
|
|
49868
|
-
if (
|
|
52085
|
+
if (existsSync19(adjPath)) {
|
|
49869
52086
|
try {
|
|
49870
|
-
const f = JSON.parse(
|
|
52087
|
+
const f = JSON.parse(readFileSync16(adjPath, "utf8"));
|
|
49871
52088
|
if (f && f.kind === "adjudication" && Array.isArray(f.items)) {
|
|
49872
52089
|
adjReady = true;
|
|
49873
52090
|
adjIds = f.items.map((i2) => i2.criteriaId);
|
|
@@ -49875,12 +52092,12 @@ function listPhases(runDir, engineAbs) {
|
|
|
49875
52092
|
} catch {
|
|
49876
52093
|
}
|
|
49877
52094
|
}
|
|
49878
|
-
const verPath =
|
|
52095
|
+
const verPath = join35(run2, "VERIFY.todo.json");
|
|
49879
52096
|
let verIds = [];
|
|
49880
52097
|
let verReady = false;
|
|
49881
|
-
if (
|
|
52098
|
+
if (existsSync19(verPath)) {
|
|
49882
52099
|
try {
|
|
49883
|
-
const items = JSON.parse(
|
|
52100
|
+
const items = JSON.parse(readFileSync16(verPath, "utf8"));
|
|
49884
52101
|
if (Array.isArray(items)) {
|
|
49885
52102
|
verReady = true;
|
|
49886
52103
|
verIds = items.map((i2) => String(i2.n));
|
|
@@ -49895,7 +52112,7 @@ function listPhases(runDir, engineAbs) {
|
|
|
49895
52112
|
worklist: adjPath,
|
|
49896
52113
|
items: adjIds.length,
|
|
49897
52114
|
ids: adjIds,
|
|
49898
|
-
prerequisite: `node ${engineAbs} verify --manual --in ${
|
|
52115
|
+
prerequisite: `node ${engineAbs} verify --manual --in ${join35(run2, "audit-latest.json")} --out ${run2}`
|
|
49899
52116
|
},
|
|
49900
52117
|
{
|
|
49901
52118
|
name: "verify-report",
|
|
@@ -49909,7 +52126,7 @@ function listPhases(runDir, engineAbs) {
|
|
|
49909
52126
|
}
|
|
49910
52127
|
function orchestrateRun(runDir, engineAbs, opts = {}) {
|
|
49911
52128
|
const run2 = resolve7(runDir);
|
|
49912
|
-
if (!
|
|
52129
|
+
if (!existsSync19(run2)) {
|
|
49913
52130
|
return { exitCode: 2, written: [], notices: [], errors: [`run dir not found: ${run2}`], phases: [] };
|
|
49914
52131
|
}
|
|
49915
52132
|
const phases = listPhases(run2, engineAbs);
|
|
@@ -49936,22 +52153,22 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
|
|
|
49936
52153
|
}
|
|
49937
52154
|
selected = [ph];
|
|
49938
52155
|
}
|
|
49939
|
-
const orchDir =
|
|
49940
|
-
const agentsDir =
|
|
49941
|
-
|
|
49942
|
-
|
|
52156
|
+
const orchDir = join35(run2, "orchestration");
|
|
52157
|
+
const agentsDir = join35(orchDir, "agents");
|
|
52158
|
+
mkdirSync10(join35(orchDir, "out"), { recursive: true });
|
|
52159
|
+
mkdirSync10(agentsDir, { recursive: true });
|
|
49943
52160
|
const written = [];
|
|
49944
52161
|
const notices = [];
|
|
49945
52162
|
for (const [name2, content] of Object.entries(agentContracts(run2, engineAbs))) {
|
|
49946
|
-
const p =
|
|
49947
|
-
|
|
52163
|
+
const p = join35(agentsDir, `${name2}.md`);
|
|
52164
|
+
writeFileSync13(p, content);
|
|
49948
52165
|
written.push(p);
|
|
49949
52166
|
}
|
|
49950
52167
|
const emitted = new Set(opts.eco ? [] : selected.filter((p) => p.items > 0).map((p) => p.name));
|
|
49951
52168
|
for (const ph of phases) {
|
|
49952
52169
|
if (emitted.has(ph.name)) continue;
|
|
49953
|
-
const stale =
|
|
49954
|
-
if (
|
|
52170
|
+
const stale = join35(orchDir, `${ph.name}.workflow.mjs`);
|
|
52171
|
+
if (existsSync19(stale)) {
|
|
49955
52172
|
rmSync4(stale, { force: true });
|
|
49956
52173
|
notices.push(`phase "${ph.name}": stale workflow removed \u2014 its worklist ${ph.ready ? "is now empty" : "no longer exists"}.`);
|
|
49957
52174
|
}
|
|
@@ -49965,13 +52182,13 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
|
|
|
49965
52182
|
if (ph.items <= SMALL_WORKLIST) {
|
|
49966
52183
|
notices.push(`phase "${ph.name}": only ${ph.items} item(s) \u2014 the sequential --eco path is equivalent and cheaper.`);
|
|
49967
52184
|
}
|
|
49968
|
-
const p =
|
|
49969
|
-
|
|
52185
|
+
const p = join35(orchDir, `${ph.name}.workflow.mjs`);
|
|
52186
|
+
writeFileSync13(p, phaseWorkflowScript(ph, run2, engineAbs, BATCH_SIZE));
|
|
49970
52187
|
written.push(p);
|
|
49971
52188
|
}
|
|
49972
52189
|
}
|
|
49973
|
-
const rb =
|
|
49974
|
-
|
|
52190
|
+
const rb = join35(orchDir, "RUNBOOK.md");
|
|
52191
|
+
writeFileSync13(rb, runbookMd(phases, run2, engineAbs));
|
|
49975
52192
|
written.push(rb);
|
|
49976
52193
|
return { exitCode: 0, written, notices, errors: [], phases };
|
|
49977
52194
|
}
|
|
@@ -49980,8 +52197,8 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
|
|
|
49980
52197
|
import { createInterface as createInterface2 } from "readline";
|
|
49981
52198
|
|
|
49982
52199
|
// src/mcp/handlers.ts
|
|
49983
|
-
import { existsSync as
|
|
49984
|
-
import { isAbsolute as isAbsolute3, join as
|
|
52200
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17, realpathSync as realpathSync2, statSync as statSync10 } from "fs";
|
|
52201
|
+
import { isAbsolute as isAbsolute3, join as join36, resolve as resolve8, sep as sep4 } from "path";
|
|
49985
52202
|
|
|
49986
52203
|
// src/project-lock.ts
|
|
49987
52204
|
var chains = /* @__PURE__ */ new Map();
|
|
@@ -50029,7 +52246,7 @@ function requiredCwd(args2, defaults) {
|
|
|
50029
52246
|
const cwd = str2(args2.cwd) ?? defaults.defaultCwd;
|
|
50030
52247
|
if (!cwd) throw new ToolError("`cwd` is required: an absolute path to the project root.");
|
|
50031
52248
|
const abs = resolve8(cwd);
|
|
50032
|
-
if (!
|
|
52249
|
+
if (!existsSync20(abs)) throw new ToolError(`project root not found: ${abs}`);
|
|
50033
52250
|
if (!statSync10(abs).isDirectory()) throw new ToolError(`\`cwd\` is not a directory: ${abs}`);
|
|
50034
52251
|
return abs;
|
|
50035
52252
|
}
|
|
@@ -50174,13 +52391,13 @@ function handlePackCheck(args2, cwd) {
|
|
|
50174
52391
|
return { cwd, ...res };
|
|
50175
52392
|
}
|
|
50176
52393
|
function handleSampleCheck(cwd) {
|
|
50177
|
-
const file =
|
|
50178
|
-
if (!
|
|
52394
|
+
const file = join36(cwd, ".ultra11yrc.json");
|
|
52395
|
+
if (!existsSync20(file)) {
|
|
50179
52396
|
throw new ToolError(`no .ultra11yrc.json at ${cwd} \u2014 a page sample must be declared before it can be linted.`);
|
|
50180
52397
|
}
|
|
50181
52398
|
let raw;
|
|
50182
52399
|
try {
|
|
50183
|
-
raw = JSON.parse(
|
|
52400
|
+
raw = JSON.parse(readFileSync17(file, "utf8"));
|
|
50184
52401
|
} catch (e) {
|
|
50185
52402
|
throw new ToolError(`.ultra11yrc.json is not valid JSON: ${e.message}`);
|
|
50186
52403
|
}
|
|
@@ -50218,7 +52435,7 @@ function handleInit(args2, cwd) {
|
|
|
50218
52435
|
const enginePath = resolve8("scripts/ultra11y.mjs");
|
|
50219
52436
|
const written = [];
|
|
50220
52437
|
if (bool(args2.hook)) written.push(writeHook(cwd, enginePath, failOn));
|
|
50221
|
-
if (bool(args2.ci)) written.push(
|
|
52438
|
+
if (bool(args2.ci)) written.push(join36(cwd, ".github/workflows/a11y.yml"));
|
|
50222
52439
|
if (!written.length) {
|
|
50223
52440
|
throw new ToolError("nothing to install \u2014 pass hook:true and/or ci:true.");
|
|
50224
52441
|
}
|
|
@@ -50238,8 +52455,8 @@ function reportText(args2, tool) {
|
|
|
50238
52455
|
if (inline) return inline;
|
|
50239
52456
|
if (!file) throw new ToolError(`\`report_text\` is required \u2014 the report markdown for ultra11y_${tool} to work on.`);
|
|
50240
52457
|
if (!isAbsolute3(file)) throw new ToolError("`report_file` must be an absolute path.");
|
|
50241
|
-
if (!
|
|
50242
|
-
return
|
|
52458
|
+
if (!existsSync20(file)) throw new ToolError(`report file not found: ${file}`);
|
|
52459
|
+
return readFileSync17(file, "utf8");
|
|
50243
52460
|
}
|
|
50244
52461
|
function handleCriteria(args2) {
|
|
50245
52462
|
const lang = langOf(args2);
|
|
@@ -50254,7 +52471,7 @@ function handleCriteria(args2) {
|
|
|
50254
52471
|
function handleRead(args2, cwd) {
|
|
50255
52472
|
const raw = str2(args2.path);
|
|
50256
52473
|
if (!raw) throw new ToolError("`path` is required \u2014 relative to the project root, or an absolute path inside it.");
|
|
50257
|
-
const target = isAbsolute3(raw) ? raw :
|
|
52474
|
+
const target = isAbsolute3(raw) ? raw : join36(cwd, raw);
|
|
50258
52475
|
let real;
|
|
50259
52476
|
try {
|
|
50260
52477
|
real = realpathSync2(target);
|
|
@@ -50268,7 +52485,7 @@ function handleRead(args2, cwd) {
|
|
|
50268
52485
|
const st = statSync10(real);
|
|
50269
52486
|
if (!st.isFile()) throw new ToolError(`not a file: ${raw}`);
|
|
50270
52487
|
if (st.size > MAX_READ_BYTES) throw new ToolError(`file is too large to read (${st.size} bytes): ${raw}`);
|
|
50271
|
-
const lines =
|
|
52488
|
+
const lines = readFileSync17(real, "utf8").split("\n");
|
|
50272
52489
|
const total = lines.length;
|
|
50273
52490
|
const start2 = Math.max(1, Math.floor(num2(args2.start_line) ?? 1));
|
|
50274
52491
|
if (start2 > total) throw new ToolError(`start_line ${start2} is past the end of the file (${total} lines).`);
|
|
@@ -50763,25 +52980,25 @@ function str3(v) {
|
|
|
50763
52980
|
var DECLARED = new Set([...TOOLS2, ...WRITE_TOOLS].map((t2) => t2.name));
|
|
50764
52981
|
|
|
50765
52982
|
// src/mcp/resources.ts
|
|
50766
|
-
import { existsSync as
|
|
50767
|
-
import { basename as basename3, dirname as
|
|
52983
|
+
import { existsSync as existsSync21, readdirSync as readdirSync6, readFileSync as readFileSync18, realpathSync as realpathSync3, statSync as statSync11 } from "fs";
|
|
52984
|
+
import { basename as basename3, dirname as dirname10, join as join37, resolve as resolve9, sep as sep5 } from "path";
|
|
50768
52985
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
50769
52986
|
var SKILL_NAME = "ultra11y";
|
|
50770
52987
|
var URI_SCHEME = "skill://";
|
|
50771
52988
|
function resolveSkillRoot(moduleDir) {
|
|
50772
|
-
const here = moduleDir ??
|
|
52989
|
+
const here = moduleDir ?? dirname10(fileURLToPath4(import.meta.url));
|
|
50773
52990
|
const candidates2 = [resolve9(here, ".."), resolve9(here, "..", "skills", SKILL_NAME), resolve9(here, "..", "..", "skills", SKILL_NAME)];
|
|
50774
|
-
return candidates2.find((dir) =>
|
|
52991
|
+
return candidates2.find((dir) => existsSync21(join37(dir, "SKILL.md")));
|
|
50775
52992
|
}
|
|
50776
52993
|
function listResources(moduleDir) {
|
|
50777
52994
|
const root = resolveSkillRoot(moduleDir);
|
|
50778
52995
|
if (!root) return [];
|
|
50779
52996
|
const out2 = [describe(root, "SKILL.md", `${SKILL_NAME}: the skill`)];
|
|
50780
|
-
const refDir =
|
|
50781
|
-
if (!
|
|
50782
|
-
for (const file of
|
|
52997
|
+
const refDir = join37(root, "references");
|
|
52998
|
+
if (!existsSync21(refDir)) return out2;
|
|
52999
|
+
for (const file of readdirSync6(refDir).sort()) {
|
|
50783
53000
|
if (!file.endsWith(".md")) continue;
|
|
50784
|
-
out2.push(describe(root,
|
|
53001
|
+
out2.push(describe(root, join37("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
|
|
50785
53002
|
}
|
|
50786
53003
|
return out2;
|
|
50787
53004
|
}
|
|
@@ -50805,7 +53022,7 @@ function readResource(uri, moduleDir) {
|
|
|
50805
53022
|
throw new ResourceError(`resource path escapes the skill root: ${uri}`);
|
|
50806
53023
|
}
|
|
50807
53024
|
if (!statSync11(targetReal).isFile()) throw new ResourceError(`not a file: ${uri}`);
|
|
50808
|
-
return { uri, mimeType: "text/markdown", text:
|
|
53025
|
+
return { uri, mimeType: "text/markdown", text: readFileSync18(targetReal, "utf8") };
|
|
50809
53026
|
}
|
|
50810
53027
|
var ResourceError = class extends Error {
|
|
50811
53028
|
};
|
|
@@ -50816,14 +53033,14 @@ function describe(root, rel, fallbackTitle) {
|
|
|
50816
53033
|
title: fallbackTitle,
|
|
50817
53034
|
mimeType: "text/markdown"
|
|
50818
53035
|
};
|
|
50819
|
-
const summary = firstProse(
|
|
53036
|
+
const summary = firstProse(join37(root, rel));
|
|
50820
53037
|
if (summary) decl.description = summary;
|
|
50821
53038
|
return decl;
|
|
50822
53039
|
}
|
|
50823
53040
|
function firstProse(file) {
|
|
50824
53041
|
let text;
|
|
50825
53042
|
try {
|
|
50826
|
-
text =
|
|
53043
|
+
text = readFileSync18(file, "utf8");
|
|
50827
53044
|
} catch {
|
|
50828
53045
|
return void 0;
|
|
50829
53046
|
}
|
|
@@ -50842,7 +53059,7 @@ var ERR_INVALID_REQUEST = -32600;
|
|
|
50842
53059
|
var ERR_METHOD_NOT_FOUND = -32601;
|
|
50843
53060
|
var ERR_INVALID_PARAMS = -32602;
|
|
50844
53061
|
var ERR_INTERNAL = -32603;
|
|
50845
|
-
function
|
|
53062
|
+
function createServer2(opts = {}) {
|
|
50846
53063
|
const serverInfo = { name: opts.serverName ?? "ultra11y", version: VERSION };
|
|
50847
53064
|
const maxBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES2;
|
|
50848
53065
|
let protocol = LATEST_PROTOCOL2;
|
|
@@ -50993,7 +53210,7 @@ async function runStdioServer(opts = {}) {
|
|
|
50993
53210
|
process.stdout.write = original;
|
|
50994
53211
|
};
|
|
50995
53212
|
}
|
|
50996
|
-
const server =
|
|
53213
|
+
const server = createServer2(opts);
|
|
50997
53214
|
const send = (msg) => {
|
|
50998
53215
|
emit2(JSON.stringify(msg) + "\n");
|
|
50999
53216
|
};
|
|
@@ -51142,7 +53359,7 @@ async function route(req, res, opts) {
|
|
|
51142
53359
|
const protocol = declared ?? ASSUMED_HTTP_PROTOCOL;
|
|
51143
53360
|
let raw;
|
|
51144
53361
|
try {
|
|
51145
|
-
raw = await
|
|
53362
|
+
raw = await readBody2(req);
|
|
51146
53363
|
} catch (e) {
|
|
51147
53364
|
if (e.message === "too large") {
|
|
51148
53365
|
sendJson(res, 413, { error: `request body exceeds ${MAX_BODY_BYTES} bytes` }, origin);
|
|
@@ -51158,7 +53375,7 @@ async function route(req, res, opts) {
|
|
|
51158
53375
|
sendJson(res, 200, { jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } }, origin);
|
|
51159
53376
|
return;
|
|
51160
53377
|
}
|
|
51161
|
-
const mcp =
|
|
53378
|
+
const mcp = createServer2(opts);
|
|
51162
53379
|
mcp.setProtocolVersion(protocol);
|
|
51163
53380
|
const out2 = [];
|
|
51164
53381
|
const collect2 = (m) => void out2.push(m);
|
|
@@ -51189,7 +53406,7 @@ function sendJson(res, status, body2, origin, extra = {}) {
|
|
|
51189
53406
|
res.end(text);
|
|
51190
53407
|
}
|
|
51191
53408
|
var DRAIN_LIMIT = MAX_BODY_BYTES * 8;
|
|
51192
|
-
function
|
|
53409
|
+
function readBody2(req) {
|
|
51193
53410
|
return new Promise((resolve10, reject) => {
|
|
51194
53411
|
const chunks = [];
|
|
51195
53412
|
let size = 0;
|
|
@@ -51232,11 +53449,13 @@ non-conformities. RGAA (France) and other country standards are pluggable packs
|
|
|
51232
53449
|
Usage:
|
|
51233
53450
|
ultra11y audit <globs\u2026 | -> [--out <dir>] [--include <glob>] [--exclude <glob>] [--ext <list>] [--jsx] [--graph] [--json] [--lang auto|en|fr] [--no-default-excludes]
|
|
51234
53451
|
ultra11y audit [--changed | --since <ref> | --staged] [--max-files <n>] [--dedup exact|normalized|off] [--baseline <file>] [--fail-on blocking|major|minor]
|
|
51235
|
-
ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures: audit real HTML
|
|
51236
|
-
ultra11y
|
|
53452
|
+
ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures + .ultra11y/pages snapshots: audit real HTML)
|
|
53453
|
+
ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
|
|
53454
|
+
ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
|
|
51237
53455
|
ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--gh-issues | --gh-single] [--lang auto|en|fr]
|
|
51238
|
-
ultra11y render [<dir>] [--scaffold | --setup | --coverage | --storybook] [--captures <dir>] [--out <file>] [--json] [--lang auto|en|fr]
|
|
53456
|
+
ultra11y render [<dir>] [--scaffold | --setup | --e2e | --coverage | --storybook] [--runner playwright|cypress|auto] [--captures <dir>] [--out <file>] [--json] [--lang auto|en|fr]
|
|
51239
53457
|
ultra11y criteria [<sc>] [--list] [--standard <pack> [--theme <N>]] [--generate] [--json] [--lang auto|en|fr]
|
|
53458
|
+
ultra11y criteria --standard <pack> --glossary [<term>] (the terms the standard DEFINES \u2014 its tests depend on them)
|
|
51240
53459
|
ultra11y check --report <md> [--standard <pack>] [--in <audit.json>] [--semantic [--verdicts <file>]] [--quiet] [--json]
|
|
51241
53460
|
ultra11y verify --report <md> [--standard <pack>] [--semantic] [--apply <verdicts.json>] [--max-verify <n>] [--out <dir>] [--json]
|
|
51242
53461
|
ultra11y verify --report <md> --in <audit.json> --manual [--out <dir>] [--json] (adjudicate the manual criteria)
|
|
@@ -51250,6 +53469,12 @@ Usage:
|
|
|
51250
53469
|
ultra11y scan --sitemap <url> | --crawl <url> [--depth <n>] [--max <n>] [--runtime \u2026] [--cwd <dir>] [--merge <audit.json>] [--json]
|
|
51251
53470
|
ultra11y scan --clean (remove the dynamic-tier Docker image + temp contexts)
|
|
51252
53471
|
ultra11y sample check [--standard <pack>] [--json] (lint the .ultra11yrc.json page sample vs the standard's required page kinds)
|
|
53472
|
+
ultra11y snapshot write [--root <dir>] [--fail-on blocking|major|minor] [--json] (payload on stdin \u2192 .ultra11y/pages/<id>/ + audit it)
|
|
53473
|
+
ultra11y snapshot list [--root <dir>] [--json]
|
|
53474
|
+
ultra11y pages --in <audit.json> [--standard <pack>] [--json] [--lang auto|en|fr] (the per-page criterion grid)
|
|
53475
|
+
ultra11y mcp [--transport stdio|http] [--cwd <dir>] [--allow-write] [--port <n>] [--bind <addr>] [--allow-remote] [--allow-origin <o>] [--max-response-bytes <n>]
|
|
53476
|
+
ultra11y dev [--port <n>] [--root <dir>] [--standard <pack>] [--lang auto|en|fr] (dev side-car: live overlay + per-page dashboard)
|
|
53477
|
+
ultra11y dev --next [--port <n>] (write the Next overlay component, then wire one line into your layout)
|
|
51253
53478
|
|
|
51254
53479
|
Commands:
|
|
51255
53480
|
audit Run the static engine over the inputs (files/globs, or '-' for stdin)
|
|
@@ -51258,12 +53483,19 @@ Commands:
|
|
|
51258
53483
|
(default auto: repo <html lang> \u2192 the active standard's default locale
|
|
51259
53484
|
\u2192 English). The engine decides the machine-detectable criteria; the AI
|
|
51260
53485
|
agent adjudicates the judgment ones (verify --manual, gated) and the
|
|
51261
|
-
scan tier decides the needs-rendering ones.
|
|
53486
|
+
scan tier decides the needs-rendering ones. --format sarif|github emits
|
|
53487
|
+
the CI rendering instead of the summary (in --baseline gate mode it
|
|
53488
|
+
covers exactly the NEW findings, i.e. what the PR introduced).
|
|
51262
53489
|
report Render an AuditResult into a dated WCAG 2.2 AA compliance report
|
|
51263
53490
|
(audits/wcag-YYYY-MM-DD.md): metadata, per-guideline synthesis table,
|
|
51264
53491
|
non-conformities by priority, conforming + not-applicable lists.
|
|
51265
53492
|
--standard <pack> writes a derived report for a country standard
|
|
51266
53493
|
(e.g. --standard rgaa \u2192 audits/rgaa-YYYY-MM-DD.md).
|
|
53494
|
+
--format renders the same result for CI instead of Markdown: 'sarif'
|
|
53495
|
+
(SARIF 2.1.0 for GitHub code scanning, so findings land as inline PR
|
|
53496
|
+
annotations) or 'github' (::error:: workflow commands on stdout plus a
|
|
53497
|
+
job summary appended to $GITHUB_STEP_SUMMARY). Both honour --standard,
|
|
53498
|
+
so this is how you get an RGAA-keyed SARIF \u2014 audit itself is WCAG-keyed.
|
|
51267
53499
|
prd Turn an AuditResult into an AUDITOR conformance backlog
|
|
51268
53500
|
(audits/prd-YYYY-MM-DD.md), one entry per criterion rendered with the
|
|
51269
53501
|
active standard's vocabulary (RGAA "Th\xE9matique/Crit\xE8re/Test", WCAG core
|
|
@@ -51278,7 +53510,12 @@ Commands:
|
|
|
51278
53510
|
react-dom/server SSR snapshot harness to fill in. --setup installs the
|
|
51279
53511
|
zero-touch test-render capture harvester (one setupFiles line \u2192 every
|
|
51280
53512
|
component your tests render is snapshotted to .ultra11y/captures and
|
|
51281
|
-
audited). --
|
|
53513
|
+
audited). --setup captures COMPONENTS from unit tests; --e2e wires the
|
|
53514
|
+
audit into an existing Playwright/Cypress run instead, so a targeted PAGE
|
|
53515
|
+
is checked during the tests you already have \u2014 each checked page is
|
|
53516
|
+
persisted to .ultra11y/pages/<id>/ (DOM + computed styles + boxes) and can
|
|
53517
|
+
be re-audited later, offline, with no browser.
|
|
53518
|
+
--coverage reports which components have a rendered capture vs
|
|
51282
53519
|
which are still opaque-source-only blind spots. --storybook attributes
|
|
51283
53520
|
per-story HTML (via a storybook-static index) back to source components.
|
|
51284
53521
|
Then audit the produced HTML, and use scan for the needs-rendering criteria.
|
|
@@ -51286,6 +53523,12 @@ Commands:
|
|
|
51286
53523
|
(criteria 1.4.3) or the full list grouped by guideline (--list).
|
|
51287
53524
|
--standard <pack>: a pack criterion, a pack theme (--theme N), or its
|
|
51288
53525
|
theme list. Carries the WCAG\u2194pack cross-refs + automatability class.
|
|
53526
|
+
--glossary [<term>] looks up a term the standard DEFINES (RGAA ships
|
|
53527
|
+
119). These definitions are normative \u2014 what "if necessary" or
|
|
53528
|
+
"relevant" mean in a test is decided there \u2014 and the adjudication
|
|
53529
|
+
worklist now inlines the ones each criterion's own tests cite. With no
|
|
53530
|
+
term, lists them all. Resolves by anchor or title, accent-insensitive;
|
|
53531
|
+
an unknown term errors with suggestions rather than a near-miss.
|
|
51289
53532
|
check Integrity gate on a produced report: every cited criterion resolves,
|
|
51290
53533
|
every NA is justified, sections + pass-rate maths are well-formed.
|
|
51291
53534
|
--standard tells it which id grammar/registry to validate against.
|
|
@@ -51348,6 +53591,38 @@ Commands:
|
|
|
51348
53591
|
page kinds (RGAA: accueil, contact, mentions l\xE9gales, d\xE9claration
|
|
51349
53592
|
d'accessibilit\xE9, plan du site, aide, authentification, pages
|
|
51350
53593
|
repr\xE9sentatives + \xE9l\xE9ments transverses) \u2014 advisory, never a gate.
|
|
53594
|
+
snapshot Persist a rendered PAGE (.ultra11y/pages/<id>/: documentElement DOM +
|
|
53595
|
+
computed-style digest + boxes + a11y tree) and audit it. 'snapshot write'
|
|
53596
|
+
reads the collected payload on stdin, so a browser-side producer (the
|
|
53597
|
+
render --e2e fixtures, the dev overlay) needs to know nothing about the
|
|
53598
|
+
on-disk format \u2014 one process per checked page. 'snapshot list' shows what
|
|
53599
|
+
has been captured. Because a snapshot is a FULL document (a component
|
|
53600
|
+
capture is a fragment), the page-scoped rules run on it: that is where
|
|
53601
|
+
html lang (RGAA 8.3) and page title (8.5/8.6) become decidable.
|
|
53602
|
+
dev Development side-car: see the defects on the page you are BUILDING,
|
|
53603
|
+
while you build it. It serves a per-page dashboard on
|
|
53604
|
+
http://127.0.0.1:4111 and an overlay the app loads; every page you
|
|
53605
|
+
visit is snapshotted, audited and listed in a floating panel, each
|
|
53606
|
+
finding linking to its file:line (Next's launch-editor endpoint).
|
|
53607
|
+
--next writes the overlay component to wire into your layout \u2014 it
|
|
53608
|
+
renders NOTHING outside development. LOOPBACK ONLY: the server
|
|
53609
|
+
writes files, so it never binds beyond 127.0.0.1.
|
|
53610
|
+
mcp Serve the engine over the Model Context Protocol, so an MCP client
|
|
53611
|
+
(Claude Code, an IDE) drives audit/report/criteria as tools instead of
|
|
53612
|
+
shelling out. --transport stdio (default) speaks over stdin/stdout;
|
|
53613
|
+
--transport http listens on 127.0.0.1 (--port, --bind), and stays
|
|
53614
|
+
loopback-only unless you pass --allow-remote. Read-only by default:
|
|
53615
|
+
--allow-write opts into the commands that touch files.
|
|
53616
|
+
pages The per-page criterion grid \u2014 RGAA is a per-page norm, the engine's
|
|
53617
|
+
verdict is scope-wide, and this bridges the two. One row per criterion
|
|
53618
|
+
(the pack's own under --standard), one column per page. Rebuilt from a
|
|
53619
|
+
committed audit.json alone: no snapshots on disk, no browser.
|
|
53620
|
+
Two honesty rules: a finding is attributed to a page only when something
|
|
53621
|
+
SAYS so (the snapshot it was raised on, the scanned URL, the sample page
|
|
53622
|
+
name, or the page's recorded source files) \u2014 anything else is reported as
|
|
53623
|
+
UNATTRIBUTED, never spread across pages; and "no finding here" means
|
|
53624
|
+
conforming only for a page whose real rendered DOM was audited. A page
|
|
53625
|
+
known only by source attribution keeps its undecided criteria "to assess".
|
|
51351
53626
|
|
|
51352
53627
|
Options:
|
|
51353
53628
|
--out <dir> output dir (report/prd/scan default: audits); for audit, persist
|
|
@@ -51390,7 +53665,19 @@ Options:
|
|
|
51390
53665
|
--coverage render: report rendered-capture coverage (covered vs blind-spot components); with --json emits the coverage object
|
|
51391
53666
|
--storybook render: attribute per-story HTML (via storybook-static index.json) into .ultra11y/captures (point the HTML dir with --captures)
|
|
51392
53667
|
--captures <dir> audit/render: rendered-capture dir to ingest (default: .ultra11y/captures)
|
|
51393
|
-
--no-captures audit: do NOT auto-detect/ingest
|
|
53668
|
+
--no-captures audit: do NOT auto-detect/ingest .ultra11y/captures nor .ultra11y/pages
|
|
53669
|
+
--e2e render: write the Playwright/Cypress fixtures into .ultra11y/e2e/
|
|
53670
|
+
--runner <name> render --e2e: force playwright|cypress instead of auto-detecting
|
|
53671
|
+
--root <dir> snapshot: project root holding .ultra11y/pages (default: .)
|
|
53672
|
+
--port <n> dev: port for the side-car (default: 4111); mcp: HTTP transport port
|
|
53673
|
+
--transport <k> mcp: stdio (default) or http
|
|
53674
|
+
--bind <addr> mcp --transport http: address to bind (default 127.0.0.1)
|
|
53675
|
+
--allow-remote mcp --transport http: accept non-loopback connections (off by default)
|
|
53676
|
+
--allow-origin <o> mcp --transport http: an Origin allowed to call the server
|
|
53677
|
+
--max-response-bytes <n> mcp: cap a tool response's size
|
|
53678
|
+
--allow-write mcp: allow the tools that write files (read-only otherwise)
|
|
53679
|
+
--glossary [<t>] criteria: look up a term the country standard defines (or list all)
|
|
53680
|
+
--next dev: write the Next.js overlay component instead of serving
|
|
51394
53681
|
--require-captures audit: gate \u2014 fail if any opaque/control component lacks a rendered capture (implies --graph)
|
|
51395
53682
|
--write fix: apply fixes to disk (default is a dry-run diff)
|
|
51396
53683
|
--iterate fix: with --write, re-audit + re-apply mechanical fixes until stable (bounded)
|
|
@@ -51462,7 +53749,25 @@ Options:
|
|
|
51462
53749
|
-v, --version print version
|
|
51463
53750
|
|
|
51464
53751
|
Data: WCAG 2.2 \xA9 W3C (W3C Document License). RGAA 4.1.2 pack \xA9 DINUM, Licence Ouverte / Etalab 2.0 (see NOTICE).`;
|
|
51465
|
-
var COMMANDS = [
|
|
53752
|
+
var COMMANDS = [
|
|
53753
|
+
"audit",
|
|
53754
|
+
"report",
|
|
53755
|
+
"prd",
|
|
53756
|
+
"render",
|
|
53757
|
+
"criteria",
|
|
53758
|
+
"check",
|
|
53759
|
+
"verify",
|
|
53760
|
+
"scan",
|
|
53761
|
+
"sample",
|
|
53762
|
+
"snapshot",
|
|
53763
|
+
"pages",
|
|
53764
|
+
"dev",
|
|
53765
|
+
"mcp",
|
|
53766
|
+
"fix",
|
|
53767
|
+
"init",
|
|
53768
|
+
"pack",
|
|
53769
|
+
"orchestrate"
|
|
53770
|
+
];
|
|
51466
53771
|
function isCommand(s) {
|
|
51467
53772
|
return !!s && COMMANDS.includes(s);
|
|
51468
53773
|
}
|
|
@@ -51500,10 +53805,14 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
|
|
|
51500
53805
|
"captures",
|
|
51501
53806
|
"run",
|
|
51502
53807
|
"phase",
|
|
53808
|
+
"root",
|
|
53809
|
+
"runner",
|
|
53810
|
+
// Shared by `dev` (side-car port) and `mcp` (HTTP transport port) — one entry, both users.
|
|
53811
|
+
"port",
|
|
53812
|
+
"glossary",
|
|
51503
53813
|
// `mcp` only. The flag sets are global, so these are accepted (and warned
|
|
51504
53814
|
// about, never silently ignored) on every command — like --phase already is.
|
|
51505
53815
|
"transport",
|
|
51506
|
-
"port",
|
|
51507
53816
|
"bind",
|
|
51508
53817
|
"allow-origin",
|
|
51509
53818
|
"max-response-bytes"
|
|
@@ -51526,6 +53835,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
51526
53835
|
"scaffold",
|
|
51527
53836
|
"storybook",
|
|
51528
53837
|
"setup",
|
|
53838
|
+
"e2e",
|
|
53839
|
+
"next",
|
|
51529
53840
|
"coverage",
|
|
51530
53841
|
"write",
|
|
51531
53842
|
"dry-run",
|
|
@@ -51632,6 +53943,36 @@ function peekMergeAudit(mergeIn) {
|
|
|
51632
53943
|
return void 0;
|
|
51633
53944
|
}
|
|
51634
53945
|
}
|
|
53946
|
+
function parseCiFormat(v) {
|
|
53947
|
+
if (v === void 0) return void 0;
|
|
53948
|
+
if (v === "sarif") return "sarif";
|
|
53949
|
+
if (v === "github") return "github";
|
|
53950
|
+
return null;
|
|
53951
|
+
}
|
|
53952
|
+
function emitCiFormat(result, format, standard, lang, failOn) {
|
|
53953
|
+
if (format === "sarif") {
|
|
53954
|
+
console.log(JSON.stringify(toSarif(result, { standard, lang }), null, 2));
|
|
53955
|
+
return;
|
|
53956
|
+
}
|
|
53957
|
+
for (const line of annotations(result, { standard, lang, failOn })) console.log(line);
|
|
53958
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
53959
|
+
const md = stepSummary(result, { standard, lang });
|
|
53960
|
+
if (summaryPath) {
|
|
53961
|
+
try {
|
|
53962
|
+
appendFileSync(summaryPath, `${md}
|
|
53963
|
+
`);
|
|
53964
|
+
} catch {
|
|
53965
|
+
}
|
|
53966
|
+
} else {
|
|
53967
|
+
console.error(md);
|
|
53968
|
+
}
|
|
53969
|
+
if (process.env.ULTRA11Y_PR_COMMENT === "1") {
|
|
53970
|
+
const c2 = pushPrComment(md, standard);
|
|
53971
|
+
console.error(
|
|
53972
|
+
c2.ok ? lang === "fr" ? `ultra11y : commentaire de PR ${c2.action === "updated" ? "mis \xE0 jour" : c2.action === "created" ? "cr\xE9\xE9" : "ignor\xE9"}${c2.reason ? ` (${c2.reason})` : ""}.` : `ultra11y: PR comment ${c2.action}${c2.reason ? ` (${c2.reason})` : ""}.` : lang === "fr" ? `ultra11y : commentaire de PR impossible${c2.reason ? ` \u2014 ${c2.reason}` : ""}.` : `ultra11y: PR comment failed${c2.reason ? ` \u2014 ${c2.reason}` : ""}.`
|
|
53973
|
+
);
|
|
53974
|
+
}
|
|
53975
|
+
}
|
|
51635
53976
|
async function cmdAudit(p) {
|
|
51636
53977
|
const inputs = p.positionals.length ? p.positionals : ["."];
|
|
51637
53978
|
if (inputs.length === 0) {
|
|
@@ -51646,13 +53987,19 @@ async function cmdAudit(p) {
|
|
|
51646
53987
|
const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
|
|
51647
53988
|
const capturesDir = capturesFlag ?? ".ultra11y/captures";
|
|
51648
53989
|
const scopedToDiff = p.flags.changed === true || p.flags.staged === true || since !== void 0;
|
|
51649
|
-
const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 ||
|
|
53990
|
+
const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 || existsSync22(capturesDir));
|
|
51650
53991
|
const useCaptures = capturesWanted && !scopedToDiff && !inputs.includes(capturesDir);
|
|
51651
|
-
const
|
|
53992
|
+
const pagesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && existsSync22(PAGES_DIR);
|
|
53993
|
+
const usePages = pagesWanted && !scopedToDiff && !inputs.includes(PAGES_DIR);
|
|
53994
|
+
const auditInputs = [...inputs, ...useCaptures ? [capturesDir] : [], ...usePages ? [PAGES_DIR] : []];
|
|
51652
53995
|
if (useCaptures)
|
|
51653
53996
|
console.error(
|
|
51654
53997
|
lang === "fr" ? `ultra11y audit : captures rendues ing\xE9r\xE9es depuis ${capturesDir}.` : `ultra11y audit: ingesting rendered captures from ${capturesDir}.`
|
|
51655
53998
|
);
|
|
53999
|
+
if (usePages)
|
|
54000
|
+
console.error(
|
|
54001
|
+
lang === "fr" ? `ultra11y audit : instantan\xE9s de page ing\xE9r\xE9s depuis ${PAGES_DIR}.` : `ultra11y audit: ingesting page snapshots from ${PAGES_DIR}.`
|
|
54002
|
+
);
|
|
51656
54003
|
const result = runAudit({
|
|
51657
54004
|
inputs: auditInputs,
|
|
51658
54005
|
stdin,
|
|
@@ -51673,13 +54020,20 @@ async function cmdAudit(p) {
|
|
|
51673
54020
|
onWarn: (m) => console.error(m)
|
|
51674
54021
|
});
|
|
51675
54022
|
lang = resolveLang(p.flags, { audit: result });
|
|
54023
|
+
if (usePages) {
|
|
54024
|
+
const scope = pageScopesFrom(readSnapshots("."));
|
|
54025
|
+
if (scope.length) {
|
|
54026
|
+
result.scope.pages = scope;
|
|
54027
|
+
attributePages(result, scope);
|
|
54028
|
+
}
|
|
54029
|
+
}
|
|
51676
54030
|
if (typeof p.flags.out === "string") {
|
|
51677
54031
|
const out2 = p.flags.out;
|
|
51678
54032
|
const asFile = out2.toLowerCase().endsWith(".json");
|
|
51679
|
-
const target = asFile ? out2 :
|
|
54033
|
+
const target = asFile ? out2 : join38(out2, "audit-latest.json");
|
|
51680
54034
|
try {
|
|
51681
|
-
|
|
51682
|
-
|
|
54035
|
+
mkdirSync11(asFile ? dirname11(out2) : out2, { recursive: true });
|
|
54036
|
+
writeFileSync14(target, JSON.stringify(result, null, 2) + "\n");
|
|
51683
54037
|
console.error(lang === "fr" ? `\u2192 audit \xE9crit dans ${target}` : `\u2192 audit written to ${target}`);
|
|
51684
54038
|
} catch {
|
|
51685
54039
|
}
|
|
@@ -51690,10 +54044,15 @@ async function cmdAudit(p) {
|
|
|
51690
54044
|
console.error(`ultra11y audit: --fail-on must be blocking|major|minor (got "${String(failOnRaw)}").`);
|
|
51691
54045
|
return 2;
|
|
51692
54046
|
}
|
|
54047
|
+
const ciFormat = parseCiFormat(p.flags.format);
|
|
54048
|
+
if (ciFormat === null) {
|
|
54049
|
+
console.error(`ultra11y audit: --format must be sarif|github (got "${String(p.flags.format)}").`);
|
|
54050
|
+
return 2;
|
|
54051
|
+
}
|
|
51693
54052
|
const baselineFlag = p.flags.baseline;
|
|
51694
54053
|
if (typeof baselineFlag === "string" && baselineFlag) {
|
|
51695
54054
|
let baseline = null;
|
|
51696
|
-
if (
|
|
54055
|
+
if (existsSync22(baselineFlag)) {
|
|
51697
54056
|
try {
|
|
51698
54057
|
const parsed = JSON.parse(readText(baselineFlag));
|
|
51699
54058
|
if (isCurrentAudit(parsed)) baseline = parsed;
|
|
@@ -51707,7 +54066,8 @@ async function cmdAudit(p) {
|
|
|
51707
54066
|
}
|
|
51708
54067
|
const diff = diffAgainstBaseline(result, baseline, failOnParsed ?? "bloquant");
|
|
51709
54068
|
const blindSpots2 = requireCaptures ? result.scope.captureCoverage?.blindSpots ?? [] : [];
|
|
51710
|
-
if (
|
|
54069
|
+
if (ciFormat) emitCiFormat({ ...result, findings: diff.newFindings }, ciFormat, CORE2, lang, failOnParsed ?? "bloquant");
|
|
54070
|
+
else if (p.flags.json)
|
|
51711
54071
|
console.log(JSON.stringify(requireCaptures && result.scope.captureCoverage ? { ...diff, captureCoverage: result.scope.captureCoverage } : diff, null, 2));
|
|
51712
54072
|
else {
|
|
51713
54073
|
console.log(baselineSummary(diff, lang));
|
|
@@ -51719,7 +54079,8 @@ async function cmdAudit(p) {
|
|
|
51719
54079
|
const failOn = failOnSet ? failOnParsed ?? "bloquant" : void 0;
|
|
51720
54080
|
const failing = failOn ? findingsAtOrAbove(result.findings, failOn) : [];
|
|
51721
54081
|
const blindSpots = requireCaptures ? result.scope.captureCoverage?.blindSpots ?? [] : [];
|
|
51722
|
-
if (
|
|
54082
|
+
if (ciFormat) emitCiFormat(result, ciFormat, CORE2, lang, failOn);
|
|
54083
|
+
else if (p.flags.json) console.log(JSON.stringify(result, null, 2));
|
|
51723
54084
|
else {
|
|
51724
54085
|
console.log(auditSummary(result, lang));
|
|
51725
54086
|
if (requireCaptures && result.scope.captureCoverage) console.error(captureCoverageSummary(result.scope.captureCoverage, lang));
|
|
@@ -51733,6 +54094,175 @@ async function cmdAudit(p) {
|
|
|
51733
54094
|
if (!failOnSet && !requireCaptures) return 0;
|
|
51734
54095
|
return failing.length || blindSpots.length ? 1 : 0;
|
|
51735
54096
|
}
|
|
54097
|
+
async function cmdDev(p) {
|
|
54098
|
+
const root = typeof p.flags.root === "string" && p.flags.root ? p.flags.root : ".";
|
|
54099
|
+
const standard = stdOf(p, "dev");
|
|
54100
|
+
if (standard === null) return 2;
|
|
54101
|
+
const lang = resolveLang(p.flags, { standard });
|
|
54102
|
+
const portRaw = p.flags.port;
|
|
54103
|
+
const port = typeof portRaw === "string" ? Number.parseInt(portRaw, 10) : DEV_DEFAULT_PORT;
|
|
54104
|
+
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
|
54105
|
+
console.error(`ultra11y dev: --port must be a number between 0 and 65535 (got "${String(portRaw)}").`);
|
|
54106
|
+
return 2;
|
|
54107
|
+
}
|
|
54108
|
+
if (p.flags.next === true) {
|
|
54109
|
+
const dir = join38(root, ".ultra11y", "next");
|
|
54110
|
+
const rel = ".ultra11y/next/overlay.jsx";
|
|
54111
|
+
try {
|
|
54112
|
+
mkdirSync11(dir, { recursive: true });
|
|
54113
|
+
writeFileSync14(join38(dir, "overlay.jsx"), nextOverlayComponent(port));
|
|
54114
|
+
} catch (e) {
|
|
54115
|
+
console.error(`ultra11y dev: could not write ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
54116
|
+
return 1;
|
|
54117
|
+
}
|
|
54118
|
+
const fr2 = lang === "fr";
|
|
54119
|
+
console.log(fr2 ? `Composant overlay \xE9crit : ${rel}` : `Overlay component written: ${rel}`);
|
|
54120
|
+
console.log("");
|
|
54121
|
+
console.log(fr2 ? "Ajoutez-le \xE0 votre layout racine (app/layout.tsx) :" : "Add it to your root layout (app/layout.tsx):");
|
|
54122
|
+
console.log(` import { Ultra11yOverlay } from "../${rel.replace(/\.jsx$/, "")}";`);
|
|
54123
|
+
console.log(` <body>{children}<Ultra11yOverlay /></body>`);
|
|
54124
|
+
console.log("");
|
|
54125
|
+
console.log(
|
|
54126
|
+
fr2 ? `Puis lancez le side-car : ultra11y dev (tableau de bord : http://127.0.0.1:${port})` : `Then start the side-car: ultra11y dev (dashboard: http://127.0.0.1:${port})`
|
|
54127
|
+
);
|
|
54128
|
+
console.log(fr2 ? "Le composant ne rend RIEN hors d\xE9veloppement." : "The component renders NOTHING outside development.");
|
|
54129
|
+
return 0;
|
|
54130
|
+
}
|
|
54131
|
+
const fr = lang === "fr";
|
|
54132
|
+
let server;
|
|
54133
|
+
try {
|
|
54134
|
+
server = await startDevServer({ root, port, standard, lang, onLog: (m) => console.error(m) });
|
|
54135
|
+
} catch (e) {
|
|
54136
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
54137
|
+
console.error(
|
|
54138
|
+
/EADDRINUSE/.test(msg) ? `ultra11y dev: port ${port} is already in use \u2014 pass --port <n>.` : `ultra11y dev: could not start the server: ${msg}`
|
|
54139
|
+
);
|
|
54140
|
+
return 1;
|
|
54141
|
+
}
|
|
54142
|
+
console.error(fr ? `ultra11y dev : tableau de bord sur http://127.0.0.1:${server.port}` : `ultra11y dev: dashboard on http://127.0.0.1:${server.port}`);
|
|
54143
|
+
console.error(fr ? "Boucle locale uniquement (l'outil \xE9crit des fichiers). Ctrl-C pour arr\xEAter." : "Loopback only (the tool writes files). Ctrl-C to stop.");
|
|
54144
|
+
await new Promise((resolve10) => {
|
|
54145
|
+
const stop2 = () => {
|
|
54146
|
+
void server.close().then(resolve10);
|
|
54147
|
+
};
|
|
54148
|
+
process.once("SIGINT", stop2);
|
|
54149
|
+
process.once("SIGTERM", stop2);
|
|
54150
|
+
});
|
|
54151
|
+
return 0;
|
|
54152
|
+
}
|
|
54153
|
+
async function cmdPages(p) {
|
|
54154
|
+
const inFlag = p.flags.in;
|
|
54155
|
+
if (typeof inFlag !== "string" || !inFlag) {
|
|
54156
|
+
console.error("ultra11y pages: --in <audit.json> is required ('-' for stdin).");
|
|
54157
|
+
return 2;
|
|
54158
|
+
}
|
|
54159
|
+
const standard = stdOf(p, "pages");
|
|
54160
|
+
if (standard === null) return 2;
|
|
54161
|
+
const raw = inFlag === "-" ? await readStdin() : readInputFile(inFlag, "pages", "--in");
|
|
54162
|
+
if (raw === null) return 2;
|
|
54163
|
+
let result;
|
|
54164
|
+
try {
|
|
54165
|
+
result = JSON.parse(raw);
|
|
54166
|
+
} catch {
|
|
54167
|
+
console.error("ultra11y pages: --in is not valid JSON (expected an AuditResult).");
|
|
54168
|
+
return 2;
|
|
54169
|
+
}
|
|
54170
|
+
if (!isCurrentAudit(result)) {
|
|
54171
|
+
console.error("ultra11y pages: input is not a current ultra11y AuditResult (WCAG-keyed, schema v2). Re-run `audit`.");
|
|
54172
|
+
return 2;
|
|
54173
|
+
}
|
|
54174
|
+
const lang = resolveLang(p.flags, { audit: result, standard });
|
|
54175
|
+
const scope = pagesOf(result);
|
|
54176
|
+
if (!scope.length) {
|
|
54177
|
+
console.error(
|
|
54178
|
+
lang === "fr" ? "ultra11y pages : aucune page dans le p\xE9rim\xE8tre. Capturez des instantan\xE9s (render --e2e) ou scannez un \xE9chantillon (scan --sample)." : "ultra11y pages: no page in scope. Capture snapshots (render --e2e) or scan a sample (scan --sample)."
|
|
54179
|
+
);
|
|
54180
|
+
return 1;
|
|
54181
|
+
}
|
|
54182
|
+
attributePages(result, scope);
|
|
54183
|
+
if (p.flags.json) console.log(JSON.stringify({ pages: derivePages(result, scope), unattributed: unattributedFindings(result).length }, null, 2));
|
|
54184
|
+
else console.log(renderPageGrid(result, scope, standard, lang));
|
|
54185
|
+
return 0;
|
|
54186
|
+
}
|
|
54187
|
+
async function cmdSnapshot(p) {
|
|
54188
|
+
const sub = p.positionals[0];
|
|
54189
|
+
const root = typeof p.flags.root === "string" && p.flags.root ? p.flags.root : ".";
|
|
54190
|
+
const lang = resolveLang(p.flags, {});
|
|
54191
|
+
if (sub === "list") {
|
|
54192
|
+
const snaps = readSnapshots(root);
|
|
54193
|
+
if (p.flags.json)
|
|
54194
|
+
console.log(
|
|
54195
|
+
JSON.stringify(
|
|
54196
|
+
snaps.map((s) => s.meta),
|
|
54197
|
+
null,
|
|
54198
|
+
2
|
|
54199
|
+
)
|
|
54200
|
+
);
|
|
54201
|
+
else if (!snaps.length) console.log(lang === "fr" ? `Aucun instantan\xE9 dans ${join38(root, PAGES_DIR)}.` : `No snapshot in ${join38(root, PAGES_DIR)}.`);
|
|
54202
|
+
else for (const s of snaps) console.log(`${s.meta.id} ${s.meta.name} ${s.meta.url}${s.meta.auth ? " [auth]" : ""}`);
|
|
54203
|
+
return 0;
|
|
54204
|
+
}
|
|
54205
|
+
if (sub !== "write") {
|
|
54206
|
+
console.error("ultra11y snapshot: expected `snapshot write` (payload on stdin) or `snapshot list`.");
|
|
54207
|
+
return 2;
|
|
54208
|
+
}
|
|
54209
|
+
const raw = await readStdin();
|
|
54210
|
+
if (!raw.trim()) {
|
|
54211
|
+
console.error("ultra11y snapshot write: no payload on stdin (expected {meta, dom, styles?, boxes?, axtree?}).");
|
|
54212
|
+
return 2;
|
|
54213
|
+
}
|
|
54214
|
+
let payload;
|
|
54215
|
+
try {
|
|
54216
|
+
payload = JSON.parse(raw);
|
|
54217
|
+
} catch {
|
|
54218
|
+
console.error("ultra11y snapshot write: stdin is not valid JSON.");
|
|
54219
|
+
return 2;
|
|
54220
|
+
}
|
|
54221
|
+
const v = validateSnapshotMeta(payload.meta);
|
|
54222
|
+
if (!v.ok || !v.meta) {
|
|
54223
|
+
for (const i2 of v.issues) console.error(`ultra11y snapshot write: ${i2.path} \u2014 ${i2.message}`);
|
|
54224
|
+
return 2;
|
|
54225
|
+
}
|
|
54226
|
+
if (typeof payload.dom !== "string" || !payload.dom.trim()) {
|
|
54227
|
+
console.error("ultra11y snapshot write: `dom` must be the serialized documentElement.outerHTML.");
|
|
54228
|
+
return 2;
|
|
54229
|
+
}
|
|
54230
|
+
let dir;
|
|
54231
|
+
try {
|
|
54232
|
+
dir = writeSnapshot(root, {
|
|
54233
|
+
meta: v.meta,
|
|
54234
|
+
dom: payload.dom,
|
|
54235
|
+
...payload.styles ? { styles: payload.styles } : {},
|
|
54236
|
+
...payload.boxes ? { boxes: payload.boxes } : {},
|
|
54237
|
+
...payload.axtree ? { axtree: payload.axtree } : {},
|
|
54238
|
+
...payload.css ? { css: payload.css } : {}
|
|
54239
|
+
});
|
|
54240
|
+
} catch (e) {
|
|
54241
|
+
console.error(`ultra11y snapshot write: could not write the snapshot: ${e instanceof Error ? e.message : String(e)}`);
|
|
54242
|
+
return 1;
|
|
54243
|
+
}
|
|
54244
|
+
if (typeof payload.screenshot === "string" && payload.screenshot) {
|
|
54245
|
+
try {
|
|
54246
|
+
writeFileSync14(join38(dir, "screen.png"), Buffer.from(payload.screenshot, "base64"));
|
|
54247
|
+
} catch {
|
|
54248
|
+
console.error("ultra11y snapshot write: the screenshot could not be written \u2014 the pixel tier will be skipped for this page.");
|
|
54249
|
+
}
|
|
54250
|
+
}
|
|
54251
|
+
const result = runAudit({ inputs: [join38(dir, "dom.html")], onWarn: (m) => console.error(m) });
|
|
54252
|
+
const failOnRaw = p.flags["fail-on"];
|
|
54253
|
+
const failOnParsed = parseFailOn(failOnRaw);
|
|
54254
|
+
if (failOnRaw !== void 0 && failOnParsed === null) {
|
|
54255
|
+
console.error(`ultra11y snapshot write: --fail-on must be blocking|major|minor (got "${String(failOnRaw)}").`);
|
|
54256
|
+
return 2;
|
|
54257
|
+
}
|
|
54258
|
+
const failing = failOnRaw !== void 0 ? findingsAtOrAbove(result.findings, failOnParsed ?? "bloquant").filter((f) => !f.advisory) : [];
|
|
54259
|
+
if (p.flags.json) console.log(JSON.stringify(result, null, 2));
|
|
54260
|
+
else {
|
|
54261
|
+
console.log(lang === "fr" ? `\u2192 instantan\xE9 \xE9crit dans ${dir}` : `\u2192 snapshot written to ${dir}`);
|
|
54262
|
+
console.log(auditSummary(result, lang));
|
|
54263
|
+
}
|
|
54264
|
+
return failing.length ? 1 : 0;
|
|
54265
|
+
}
|
|
51736
54266
|
function cmdInit(p) {
|
|
51737
54267
|
const root = repoRoot() ?? process.cwd();
|
|
51738
54268
|
let engineRel = process.argv[1] ?? "scripts/ultra11y.mjs";
|
|
@@ -51755,9 +54285,9 @@ function cmdInit(p) {
|
|
|
51755
54285
|
if (want.baseline) {
|
|
51756
54286
|
const inputs = p.positionals.length ? p.positionals : ["."];
|
|
51757
54287
|
const result = runAudit({ inputs, onWarn: (m) => console.error(m) });
|
|
51758
|
-
|
|
51759
|
-
const bp =
|
|
51760
|
-
|
|
54288
|
+
mkdirSync11(join38(root, "audits"), { recursive: true });
|
|
54289
|
+
const bp = join38(root, "audits", "baseline.json");
|
|
54290
|
+
writeFileSync14(bp, JSON.stringify(result, null, 2) + "\n");
|
|
51761
54291
|
wrote.push(bp);
|
|
51762
54292
|
}
|
|
51763
54293
|
if (want.hook) wrote.push(writeHook(root, engineRel, failOn, legacy ? "baseline" : "staged"));
|
|
@@ -51781,7 +54311,8 @@ function cmdCriteria(p) {
|
|
|
51781
54311
|
list: p.flags.list === true,
|
|
51782
54312
|
json: p.flags.json === true,
|
|
51783
54313
|
lang: resolveLang(p.flags, { standard }),
|
|
51784
|
-
standard
|
|
54314
|
+
standard,
|
|
54315
|
+
...p.flags.glossary !== void 0 ? { glossary: p.flags.glossary } : {}
|
|
51785
54316
|
});
|
|
51786
54317
|
}
|
|
51787
54318
|
async function cmdReport(p) {
|
|
@@ -51807,6 +54338,15 @@ async function cmdReport(p) {
|
|
|
51807
54338
|
}
|
|
51808
54339
|
const out2 = typeof p.flags.out === "string" ? p.flags.out : "audits";
|
|
51809
54340
|
const lang = resolveLang(p.flags, { audit: result, standard });
|
|
54341
|
+
const ciFormat = parseCiFormat(p.flags.format);
|
|
54342
|
+
if (ciFormat === null) {
|
|
54343
|
+
console.error(`ultra11y report: --format must be sarif|github (got "${String(p.flags.format)}").`);
|
|
54344
|
+
return 2;
|
|
54345
|
+
}
|
|
54346
|
+
if (ciFormat) {
|
|
54347
|
+
emitCiFormat(result, ciFormat, standard, lang);
|
|
54348
|
+
return 0;
|
|
54349
|
+
}
|
|
51810
54350
|
const path = writeReport(result, { out: out2, lang, standard });
|
|
51811
54351
|
const untested = isCore(standard) ? [] : untestedNeedsRendering(result);
|
|
51812
54352
|
const partial = untested.length > 0;
|
|
@@ -51883,13 +54423,67 @@ async function cmdPrd(p) {
|
|
|
51883
54423
|
if (json) console.log(JSON.stringify({ paths, units: prdUnits(result, standard, lang), ...gh2 ? { gh: gh2 } : {} }, null, 2));
|
|
51884
54424
|
return gh2 && gh2.failed > 0 ? 1 : 0;
|
|
51885
54425
|
}
|
|
54426
|
+
function depsAt(root) {
|
|
54427
|
+
const pkgPath = join38(root, "package.json");
|
|
54428
|
+
if (!existsSync22(pkgPath)) return {};
|
|
54429
|
+
try {
|
|
54430
|
+
const pkg = JSON.parse(readText(pkgPath));
|
|
54431
|
+
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
54432
|
+
} catch {
|
|
54433
|
+
return {};
|
|
54434
|
+
}
|
|
54435
|
+
}
|
|
54436
|
+
function engineRefFor(root) {
|
|
54437
|
+
let ref = process.argv[1] ?? "scripts/ultra11y.mjs";
|
|
54438
|
+
try {
|
|
54439
|
+
const abs = realpathSync4(ref);
|
|
54440
|
+
ref = abs.startsWith(root + sep6) ? relative3(root, abs) : abs;
|
|
54441
|
+
} catch {
|
|
54442
|
+
}
|
|
54443
|
+
return ref;
|
|
54444
|
+
}
|
|
51886
54445
|
function cmdRender(p) {
|
|
51887
54446
|
const root = p.positionals[0] ?? ".";
|
|
51888
54447
|
const lang = resolveLang(p.flags, {});
|
|
54448
|
+
if (p.flags.e2e === true) {
|
|
54449
|
+
const forced = typeof p.flags.runner === "string" ? p.flags.runner : void 0;
|
|
54450
|
+
if (forced !== void 0 && forced !== "playwright" && forced !== "cypress" && forced !== "auto") {
|
|
54451
|
+
console.error(`ultra11y render: --runner must be playwright|cypress|auto (got "${forced}").`);
|
|
54452
|
+
return 2;
|
|
54453
|
+
}
|
|
54454
|
+
const detected = detectE2eRunner(depsAt(root), (f) => existsSync22(join38(root, f)));
|
|
54455
|
+
const runners = forced && forced !== "auto" ? [forced] : detected;
|
|
54456
|
+
if (!runners.length) {
|
|
54457
|
+
console.error(e2eSetupPlan([], {}, lang));
|
|
54458
|
+
return 1;
|
|
54459
|
+
}
|
|
54460
|
+
const engineRef = engineRefFor(root);
|
|
54461
|
+
const dir = join38(root, ".ultra11y", "e2e");
|
|
54462
|
+
const paths = {};
|
|
54463
|
+
try {
|
|
54464
|
+
mkdirSync11(dir, { recursive: true });
|
|
54465
|
+
if (runners.includes("playwright")) {
|
|
54466
|
+
writeFileSync14(join38(dir, "playwright.mjs"), playwrightFixture(engineRef));
|
|
54467
|
+
paths.playwright = ".ultra11y/e2e/playwright.mjs";
|
|
54468
|
+
}
|
|
54469
|
+
if (runners.includes("cypress")) {
|
|
54470
|
+
writeFileSync14(join38(dir, "cypress-plugin.mjs"), cypressPlugin(engineRef));
|
|
54471
|
+
writeFileSync14(join38(dir, "cypress-commands.mjs"), cypressCommands());
|
|
54472
|
+
paths.cypressPlugin = ".ultra11y/e2e/cypress-plugin.mjs";
|
|
54473
|
+
paths.cypressCommands = ".ultra11y/e2e/cypress-commands.mjs";
|
|
54474
|
+
}
|
|
54475
|
+
} catch (e) {
|
|
54476
|
+
console.error(`ultra11y render: could not write the E2E fixtures: ${e instanceof Error ? e.message : String(e)}`);
|
|
54477
|
+
return 1;
|
|
54478
|
+
}
|
|
54479
|
+
if (p.flags.json) console.log(JSON.stringify({ runners, paths }, null, 2));
|
|
54480
|
+
else console.log(e2eSetupPlan(runners, paths, lang));
|
|
54481
|
+
return 0;
|
|
54482
|
+
}
|
|
51889
54483
|
if (p.flags.scaffold === true) {
|
|
51890
54484
|
const out2 = typeof p.flags.out === "string" && p.flags.out ? p.flags.out : "ultra11y-render.tsx";
|
|
51891
54485
|
try {
|
|
51892
|
-
|
|
54486
|
+
writeFileSync14(out2, ssrHarness());
|
|
51893
54487
|
} catch (e) {
|
|
51894
54488
|
console.error(`ultra11y render: could not write ${out2}: ${e instanceof Error ? e.message : String(e)}`);
|
|
51895
54489
|
return 1;
|
|
@@ -51903,29 +54497,29 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
|
|
|
51903
54497
|
}
|
|
51904
54498
|
if (p.flags.setup === true) {
|
|
51905
54499
|
const rel = ".ultra11y/capture-setup.mjs";
|
|
51906
|
-
const out2 =
|
|
54500
|
+
const out2 = join38(root, rel);
|
|
51907
54501
|
try {
|
|
51908
|
-
|
|
51909
|
-
|
|
54502
|
+
mkdirSync11(dirname11(out2), { recursive: true });
|
|
54503
|
+
writeFileSync14(out2, captureSetup());
|
|
51910
54504
|
} catch (e) {
|
|
51911
54505
|
console.error(`ultra11y render: could not write ${out2}: ${e instanceof Error ? e.message : String(e)}`);
|
|
51912
54506
|
return 1;
|
|
51913
54507
|
}
|
|
51914
54508
|
let setupDeps = {};
|
|
51915
|
-
const setupPkg =
|
|
51916
|
-
if (
|
|
54509
|
+
const setupPkg = join38(root, "package.json");
|
|
54510
|
+
if (existsSync22(setupPkg)) {
|
|
51917
54511
|
try {
|
|
51918
54512
|
const pkg = JSON.parse(readText(setupPkg));
|
|
51919
54513
|
setupDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
51920
54514
|
} catch {
|
|
51921
54515
|
}
|
|
51922
54516
|
}
|
|
51923
|
-
const tr = detectTestRunner(setupDeps, (f) =>
|
|
54517
|
+
const tr = detectTestRunner(setupDeps, (f) => existsSync22(join38(root, f)));
|
|
51924
54518
|
console.log(captureSetupPlan(tr, rel, lang));
|
|
51925
54519
|
const gaLine = ".ultra11y/captures/*.html text eol=lf linguist-generated=true";
|
|
51926
|
-
const gaPath =
|
|
54520
|
+
const gaPath = join38(root, ".gitattributes");
|
|
51927
54521
|
try {
|
|
51928
|
-
const existing =
|
|
54522
|
+
const existing = existsSync22(gaPath) ? readFileSync19(gaPath, "utf8") : "";
|
|
51929
54523
|
if (!existing.includes(".ultra11y/captures/")) {
|
|
51930
54524
|
appendFileSync(gaPath, (existing && !existing.endsWith("\n") ? "\n" : "") + gaLine + "\n");
|
|
51931
54525
|
console.log(lang === "fr" ? `.gitattributes : ajout\xE9 \xAB ${gaLine} \xBB` : `.gitattributes: added "${gaLine}"`);
|
|
@@ -51933,8 +54527,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
|
|
|
51933
54527
|
} catch {
|
|
51934
54528
|
}
|
|
51935
54529
|
try {
|
|
51936
|
-
const giPath =
|
|
51937
|
-
if (
|
|
54530
|
+
const giPath = join38(root, ".gitignore");
|
|
54531
|
+
if (existsSync22(giPath) && /^\s*\/?\.ultra11y(\/\**)?\/?\s*$/m.test(readFileSync19(giPath, "utf8")))
|
|
51938
54532
|
console.error(
|
|
51939
54533
|
lang === "fr" ? "\u26A0\uFE0F .ultra11y semble ignor\xE9 par .gitignore \u2014 les captures doivent \xEAtre committ\xE9es pour le gate (ajoutez \xAB !.ultra11y/captures/ \xBB)." : '\u26A0\uFE0F .ultra11y appears gitignored \u2014 captures must be committed for the gate (add "!.ultra11y/captures/").'
|
|
51940
54534
|
);
|
|
@@ -51944,8 +54538,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
|
|
|
51944
54538
|
}
|
|
51945
54539
|
if (p.flags.storybook === true || typeof p.flags.storybook === "string") {
|
|
51946
54540
|
const sbDir = p.positionals[0] ?? "storybook-static";
|
|
51947
|
-
const indexPath =
|
|
51948
|
-
if (!
|
|
54541
|
+
const indexPath = existsSync22(join38(sbDir, "index.json")) ? join38(sbDir, "index.json") : join38(sbDir, "stories.json");
|
|
54542
|
+
if (!existsSync22(indexPath)) {
|
|
51949
54543
|
console.error(
|
|
51950
54544
|
lang === "fr" ? `ultra11y render : aucun index Storybook (index.json/stories.json) dans ${sbDir}.` : `ultra11y render: no Storybook index (index.json/stories.json) in ${sbDir}.`
|
|
51951
54545
|
);
|
|
@@ -51955,7 +54549,7 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
|
|
|
51955
54549
|
const provById = new Map(stories.map((s) => [s.id, storyProvenance(s)]));
|
|
51956
54550
|
const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
|
|
51957
54551
|
const htmlDir = capturesFlag ?? sbDir;
|
|
51958
|
-
const htmlFiles =
|
|
54552
|
+
const htmlFiles = existsSync22(htmlDir) ? discover([htmlDir]).files.filter((f) => /\.html?$/i.test(f)) : [];
|
|
51959
54553
|
const outDir = ".ultra11y/captures";
|
|
51960
54554
|
let attributed = 0;
|
|
51961
54555
|
let skipped = 0;
|
|
@@ -51977,8 +54571,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
|
|
|
51977
54571
|
continue;
|
|
51978
54572
|
}
|
|
51979
54573
|
try {
|
|
51980
|
-
|
|
51981
|
-
|
|
54574
|
+
mkdirSync11(outDir, { recursive: true });
|
|
54575
|
+
writeFileSync14(join38(outDir, `${hitId}.html`), `${formatCaptureComment(prov)}
|
|
51982
54576
|
${raw}${raw.endsWith("\n") ? "" : "\n"}`);
|
|
51983
54577
|
attributed++;
|
|
51984
54578
|
} catch {
|
|
@@ -52000,11 +54594,11 @@ ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
|
|
|
52000
54594
|
}
|
|
52001
54595
|
if (p.flags.coverage === true) {
|
|
52002
54596
|
const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
|
|
52003
|
-
const capturesDir = capturesFlag ??
|
|
54597
|
+
const capturesDir = capturesFlag ?? join38(root, ".ultra11y/captures");
|
|
52004
54598
|
const graphExt = [...GRAPH_ONLY_EXT, ...asList(p.flags.ext) ?? []];
|
|
52005
54599
|
const sourceFiles = discover([root], { include: asList(p.flags.include), exclude: asList(p.flags.exclude), ext: graphExt }).files;
|
|
52006
54600
|
const graph = buildGraphStreaming(sourceFiles);
|
|
52007
|
-
const capFiles =
|
|
54601
|
+
const capFiles = existsSync22(capturesDir) ? discover([capturesDir]).files : [];
|
|
52008
54602
|
const entries = capFiles.map((f) => ({ file: toPosix(f), provenance: parseCaptureProvenance(readText(f)) }));
|
|
52009
54603
|
const cov = computeCaptureCoverage(graph, entries);
|
|
52010
54604
|
if (p.flags.json) console.log(JSON.stringify(cov, null, 2));
|
|
@@ -52012,15 +54606,15 @@ ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
|
|
|
52012
54606
|
return 0;
|
|
52013
54607
|
}
|
|
52014
54608
|
let deps = {};
|
|
52015
|
-
const pkgPath =
|
|
52016
|
-
if (
|
|
54609
|
+
const pkgPath = join38(root, "package.json");
|
|
54610
|
+
if (existsSync22(pkgPath)) {
|
|
52017
54611
|
try {
|
|
52018
54612
|
const pkg = JSON.parse(readText(pkgPath));
|
|
52019
54613
|
deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
52020
54614
|
} catch {
|
|
52021
54615
|
}
|
|
52022
54616
|
}
|
|
52023
|
-
const detection = detectFrameworks(deps, (f) =>
|
|
54617
|
+
const detection = detectFrameworks(deps, (f) => existsSync22(join38(root, f)));
|
|
52024
54618
|
if (p.flags.json) console.log(JSON.stringify(detection, null, 2));
|
|
52025
54619
|
else console.log(renderPlan(detection, lang));
|
|
52026
54620
|
return 0;
|
|
@@ -52219,9 +54813,9 @@ function applyAdjudicationFile(p, adj, lang) {
|
|
|
52219
54813
|
return 1;
|
|
52220
54814
|
}
|
|
52221
54815
|
const out2 = typeof p.flags.out === "string" ? p.flags.out : ".";
|
|
52222
|
-
|
|
52223
|
-
const auditPath =
|
|
52224
|
-
|
|
54816
|
+
mkdirSync11(out2, { recursive: true });
|
|
54817
|
+
const auditPath = join38(out2, "audit-latest.json");
|
|
54818
|
+
writeFileSync14(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
|
|
52225
54819
|
if (p.flags.json) console.log(JSON.stringify({ ok: true, auditPath, applied: r.applied, stillManual: r.stillManual, grounding: r.grounding }, null, 2));
|
|
52226
54820
|
else
|
|
52227
54821
|
console.log(
|
|
@@ -52291,7 +54885,7 @@ async function cmdScan(p) {
|
|
|
52291
54885
|
return 0;
|
|
52292
54886
|
}
|
|
52293
54887
|
for (const target of p.positionals.filter((a) => a !== "-")) {
|
|
52294
|
-
if (/^https?:\/\//i.test(target) ||
|
|
54888
|
+
if (/^https?:\/\//i.test(target) || existsSync22(target)) continue;
|
|
52295
54889
|
console.error(
|
|
52296
54890
|
lang === "fr" ? `ultra11y scan : fichier introuvable (file not found) : ${target}. Passez une URL http(s):// ou un fichier HTML existant.` : `ultra11y scan: File not found: ${target}. Pass an http(s):// URL or an existing HTML file.`
|
|
52297
54891
|
);
|
|
@@ -52416,12 +55010,12 @@ async function cmdScan(p) {
|
|
|
52416
55010
|
audit2 = parsed;
|
|
52417
55011
|
}
|
|
52418
55012
|
const merged = mergeDynamic(audit2, dynamic, lang);
|
|
52419
|
-
|
|
52420
|
-
|
|
55013
|
+
mkdirSync11(out2, { recursive: true });
|
|
55014
|
+
writeFileSync14(join38(out2, "audit-latest.json"), JSON.stringify(merged, null, 2) + "\n");
|
|
52421
55015
|
if (p.flags.json) console.log(JSON.stringify(merged, null, 2));
|
|
52422
55016
|
else
|
|
52423
55017
|
console.log(
|
|
52424
|
-
lang === "fr" ? `Audit statique + dynamique fusionn\xE9 \u2192 ${
|
|
55018
|
+
lang === "fr" ? `Audit statique + dynamique fusionn\xE9 \u2192 ${join38(out2, "audit-latest.json")} (${merged.conformancePct}% r\xE9ussite, ${merged.findings.length} findings).` : `Static + dynamic audit merged \u2192 ${join38(out2, "audit-latest.json")} (${merged.conformancePct}% pass rate, ${merged.findings.length} findings).`
|
|
52425
55019
|
);
|
|
52426
55020
|
return 0;
|
|
52427
55021
|
}
|
|
@@ -52601,7 +55195,7 @@ function cmdOrchestrate(p) {
|
|
|
52601
55195
|
}
|
|
52602
55196
|
const engineAbs = realpathSync4(fileURLToPath5(import.meta.url));
|
|
52603
55197
|
if (p.flags.list === true) {
|
|
52604
|
-
if (!
|
|
55198
|
+
if (!existsSync22(runFlag)) {
|
|
52605
55199
|
console.error(`ultra11y orchestrate: run dir not found: ${runFlag}.`);
|
|
52606
55200
|
return 2;
|
|
52607
55201
|
}
|
|
@@ -52628,7 +55222,7 @@ function cmdOrchestrate(p) {
|
|
|
52628
55222
|
);
|
|
52629
55223
|
} else {
|
|
52630
55224
|
console.log(
|
|
52631
|
-
lang === "fr" ? `Suivez ${
|
|
55225
|
+
lang === "fr" ? `Suivez ${join38(runFlag, "orchestration", "RUNBOOK.md")} s\xE9quentiellement (chemin \xE9co).` : `Follow ${join38(runFlag, "orchestration", "RUNBOOK.md")} sequentially (the eco path).`
|
|
52632
55226
|
);
|
|
52633
55227
|
}
|
|
52634
55228
|
if (p.flags.phase === void 0 && workflows.length === 0 && p.flags.eco !== true) {
|
|
@@ -52696,6 +55290,12 @@ async function main(argv) {
|
|
|
52696
55290
|
return cmdScan(p);
|
|
52697
55291
|
case "sample":
|
|
52698
55292
|
return cmdSample(p);
|
|
55293
|
+
case "snapshot":
|
|
55294
|
+
return cmdSnapshot(p);
|
|
55295
|
+
case "pages":
|
|
55296
|
+
return cmdPages(p);
|
|
55297
|
+
case "dev":
|
|
55298
|
+
return cmdDev(p);
|
|
52699
55299
|
case "fix":
|
|
52700
55300
|
return cmdFix(p);
|
|
52701
55301
|
case "init":
|