ultra11y 5.23.0 → 5.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/README.md +2 -2
- package/package.json +1 -1
- package/scripts/build-pack-rgaa.mjs +27 -1
- package/scripts/ultra11y.mjs +247 -9
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +247 -9
- package/skills/ultra11y/SKILL.md +3 -3
- package/skills/ultra11y/references/ci.md +1 -1
- package/skills/ultra11y/references/criteria.md +1 -1
- package/skills/ultra11y/references/judgment.md +1 -1
- package/skills/ultra11y/references/mcp.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +247 -9
- package/src/adjudicate-subjects.ts +116 -2
- package/src/adjudicate.ts +11 -2
- package/src/data/standards/rgaa.json +15 -1
- package/src/data/wcag.json +4 -1
- package/src/llm.ts +1 -1
- package/src/messages.ts +42 -0
- package/src/rules/presentation.ts +165 -2
- package/src/standards/coverage.ts +1 -1
- package/src/standards/derive.ts +13 -1
- package/src/types.ts +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "ultra11y",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
|
|
13
|
-
"version": "5.
|
|
13
|
+
"version": "5.24.0",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
16
16
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.24.0",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.24.0",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
// Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
|
|
28
28
|
// does not carry it, so a user's own ultra11y.js is never clobbered.
|
|
29
29
|
const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
|
|
30
|
-
const ULTRA11Y_PLUGIN_VERSION = "5.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.24.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Full details, thresholds and off-switches under [Install](#install).</sub>
|
|
|
25
25
|
|
|
26
26
|
> Audit HTML/CSS/JSX against **WCAG 2.2 AA** accessibility and produce a dated compliance report — or author/review accessible markup without regressions. A [skills.sh](https://skills.sh) agent skill: a deterministic, zero-dependency static engine **plus** the agent's judgment, with `check`/`verify` gates against hallucinated non-conformities. **The central deliverable is the auditor conformance block** — theme, criterion + official wording, test(s), WCAG mapping + level, finding, expected state, verification, `file:line` occurrences — rendered identically by the `report` (compliance doc), the `prd` backlog and the GitHub issues, in the active standard's vocabulary and **in your language** (`--lang auto` follows the conversation/repo). **WCAG is the worldwide core; country standards (RGAA, …) are pluggable in-repo packs.**
|
|
27
27
|
|
|
28
|
-
ultra11y is built around an honest **division of labour**. Automated tools only catch a fraction of accessibility problems, so the engine does the *mechanical* work —
|
|
28
|
+
ultra11y is built around an honest **division of labour**. Automated tools only catch a fraction of accessibility problems, so the engine does the *mechanical* work — 81 machine-detectable static checks tied to the WCAG 2.2 success criteria — and is explicit about everything it can't decide statically. What it can't, the **AI agent adjudicates** (statically, from the evidence, gated by `verify`/`check`) — not a deferral to a human:
|
|
29
29
|
|
|
30
30
|
- **Automatable (engine):** missing `alt`/`lang`/`title`, unlabeled fields, empty links/buttons, icon-only controls, iframes without title, tables without headers, heading-level skips, empty/dangling headings & labels, duplicate ids, invalid/broken ARIA, positive `tabindex`, autoplay/timed-refresh/`blink`/`marquee` media…
|
|
31
31
|
- **Agent judgment (gated):** alt-text relevance, link purpose in context, reading/tab order, caption accuracy — the agent rules on these via `verify --manual` → `--apply`, each verdict carrying a justification (or a groundable NC), never a silent "conforming".
|
|
@@ -226,7 +226,7 @@ from auditing out of memory. `cwd` is optional on all five; it selects whose pac
|
|
|
226
226
|
`ultra11y_method` is the one to call first. It partitions the standard into evidence tiers —
|
|
227
227
|
`source`, `cross-file`, `rendered-page`, `browser`, `judgment`, `out-of-scope` — from the
|
|
228
228
|
engine's own per-criterion rule applicability, never from guessing at the wording of a test.
|
|
229
|
-
For RGAA it reports that **
|
|
229
|
+
For RGAA it reports that **56 of 106 criteria declare that no engine rule can evidence them**.
|
|
230
230
|
That is the standard saying they are yours, and a plan that hides it reads as coverage that
|
|
231
231
|
does not exist.
|
|
232
232
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.24.0",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
|
@@ -310,7 +310,10 @@ async function main() {
|
|
|
310
310
|
// (link explicitness, WCAG 2.4.4). ADVISORY: it surfaces as a recommendation in the RGAA
|
|
311
311
|
// projection and NEVER makes 6.1 non-conformant. This proves a pack ships its OWN
|
|
312
312
|
// detection without forking the engine (see skills/ultra11y/references/packs.md).
|
|
313
|
-
|
|
313
|
+
// Kept in step with the engine instead of retyped: two hand-maintained copies of a rule-id
|
|
314
|
+
// list are two chances for the pack to claim an instrument the engine does not ship.
|
|
315
|
+
const PRESENTATIONAL_RULE_IDS = ["presentational-element", "presentational-attribute", "presentational-spacing"];
|
|
316
|
+
const DOWNLOAD_EXT = "pdf|docx?|pptx?|xlsx?|odt|ods|odp|rtf|csv|zip|rar|7z|gz|epub|mp3|mp4|avi|mov";
|
|
314
317
|
const rules = [
|
|
315
318
|
{
|
|
316
319
|
id: "download-link-format",
|
|
@@ -423,6 +426,28 @@ async function main() {
|
|
|
423
426
|
c.appliesTo = { ruleIds: [...new Set([...c.appliesTo.ruleIds, `pack:rgaa:${rule.id}`])].sort() };
|
|
424
427
|
}
|
|
425
428
|
|
|
429
|
+
// ---- RGAA 10.1 — the engine's presentational-markup rules, and their normativity ----
|
|
430
|
+
//
|
|
431
|
+
// 10.1's three tests are « these must be absent », over lists the RGAA closes itself: the
|
|
432
|
+
// glossary entry « Présentation de l'information » names the forbidden elements and
|
|
433
|
+
// attributes. The engine ships the instruments (src/rules/presentation.ts); the pack says
|
|
434
|
+
// they decide THIS criterion, and re-normativizes them.
|
|
435
|
+
//
|
|
436
|
+
// They ship ADVISORY because `<center>` is obsolete HTML rather than a WCAG failure —
|
|
437
|
+
// nothing about it breaks assistive technology, and reporting a WCAG 1.3.1 non-conformity
|
|
438
|
+
// for it would be wrong. Under the RGAA it is a plain failure of test 10.1.1, so the pack
|
|
439
|
+
// flips it here. This is what `overrides` is for, and it is the first use of it.
|
|
440
|
+
//
|
|
441
|
+
// Before this, 10.1 had NO instrument and inherited `readingOrder` through WCAG 1.3.2 — a
|
|
442
|
+
// subject that answers a different question — so it reached the adjudicator with an empty
|
|
443
|
+
// harvest. Measured on run 32508717451 (Sonnet, RGAA, 3 passes): the model ruled `C` three
|
|
444
|
+
// times, the gate refused all three for citing nothing, and 10.1 was the ONE criterion of
|
|
445
|
+
// the 106 left « à évaluer », on the run and on every page.
|
|
446
|
+
const presentational = criteria.find((x) => x.id === "10.1");
|
|
447
|
+
if (!presentational) throw new Error("build-pack-rgaa: RGAA 10.1 is missing from the referential");
|
|
448
|
+
presentational.appliesTo = { ruleIds: [...new Set([...presentational.appliesTo.ruleIds, ...PRESENTATIONAL_RULE_IDS])].sort() };
|
|
449
|
+
const overrides = Object.fromEntries(PRESENTATIONAL_RULE_IDS.map((id) => [id, { advisory: false, severity: "majeur" }]));
|
|
450
|
+
|
|
426
451
|
const pack = {
|
|
427
452
|
key: "rgaa",
|
|
428
453
|
name: "RGAA",
|
|
@@ -505,6 +530,7 @@ async function main() {
|
|
|
505
530
|
},
|
|
506
531
|
],
|
|
507
532
|
rules,
|
|
533
|
+
overrides,
|
|
508
534
|
themes,
|
|
509
535
|
criteria,
|
|
510
536
|
};
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.
|
|
9
|
+
var VERSION = "5.24.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -311,7 +311,10 @@ var wcag_default = {
|
|
|
311
311
|
"th-no-data-cells",
|
|
312
312
|
"radio-checkbox-group-ungrouped",
|
|
313
313
|
"table-empty-data-cell",
|
|
314
|
-
"css-generated-content-informative"
|
|
314
|
+
"css-generated-content-informative",
|
|
315
|
+
"presentational-element",
|
|
316
|
+
"presentational-attribute",
|
|
317
|
+
"presentational-spacing"
|
|
315
318
|
],
|
|
316
319
|
understanding: "https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html",
|
|
317
320
|
techniques: [
|
|
@@ -38119,6 +38122,36 @@ var MSG_CATALOG = {
|
|
|
38119
38122
|
en: () => `Remove user-scalable=no and maximum-scale (or set maximum-scale \u2265 2) from the viewport content.`
|
|
38120
38123
|
}
|
|
38121
38124
|
},
|
|
38125
|
+
"presentational-element": {
|
|
38126
|
+
message: {
|
|
38127
|
+
fr: (p) => `<${p.tag}> est une balise de pr\xE9sentation \u2014 la mise en forme doit passer par CSS (test RGAA 10.1.1).`,
|
|
38128
|
+
en: (p) => `<${p.tag}> is a presentational element \u2014 styling belongs in CSS (RGAA test 10.1.1).`
|
|
38129
|
+
},
|
|
38130
|
+
remediation: {
|
|
38131
|
+
fr: (p) => `Remplacez <${p.tag}> par un \xE9l\xE9ment porteur de sens (ou un <span>/<div> neutre) et d\xE9placez la mise en forme dans la feuille de styles.`,
|
|
38132
|
+
en: (p) => `Replace <${p.tag}> with a meaningful element (or a neutral <span>/<div>) and move the styling to the stylesheet.`
|
|
38133
|
+
}
|
|
38134
|
+
},
|
|
38135
|
+
"presentational-attribute": {
|
|
38136
|
+
message: {
|
|
38137
|
+
fr: (p) => `Attribut de pr\xE9sentation ${p.attr} sur <${p.tag}> \u2014 la mise en forme doit passer par CSS (test RGAA 10.1.2).`,
|
|
38138
|
+
en: (p) => `Presentational attribute ${p.attr} on <${p.tag}> \u2014 styling belongs in CSS (RGAA test 10.1.2).`
|
|
38139
|
+
},
|
|
38140
|
+
remediation: {
|
|
38141
|
+
fr: (p) => `Retirez ${p.attr} de <${p.tag}> et exprimez la m\xEAme mise en forme dans la feuille de styles.`,
|
|
38142
|
+
en: (p) => `Remove ${p.attr} from <${p.tag}> and express the same styling in the stylesheet.`
|
|
38143
|
+
}
|
|
38144
|
+
},
|
|
38145
|
+
"presentational-spacing": {
|
|
38146
|
+
message: {
|
|
38147
|
+
fr: (p) => p.how === "spelled-out" ? `Mot dont les lettres sont s\xE9par\xE9es par des espaces \u2014 restitu\xE9 lettre \xE0 lettre par un lecteur d'\xE9cran (test RGAA 10.1.3).` : `Suite d'espaces ins\xE9cables utilis\xE9e comme mise en page (test RGAA 10.1.3).`,
|
|
38148
|
+
en: (p) => p.how === "spelled-out" ? `A word whose letters are separated by spaces \u2014 a screen reader reads it out letter by letter (RGAA test 10.1.3).` : `A run of non-breaking spaces used as layout (RGAA test 10.1.3).`
|
|
38149
|
+
},
|
|
38150
|
+
remediation: {
|
|
38151
|
+
fr: (p) => p.how === "spelled-out" ? `\xC9crivez le mot normalement et obtenez l'espacement avec letter-spacing en CSS.` : `Remplacez les espaces ins\xE9cables par une mise en page CSS (grille, colonnes, marges).`,
|
|
38152
|
+
en: (p) => p.how === "spelled-out" ? `Write the word normally and get the spacing from CSS letter-spacing.` : `Replace the non-breaking spaces with CSS layout (grid, columns, margins).`
|
|
38153
|
+
}
|
|
38154
|
+
},
|
|
38122
38155
|
"css-generated-content-informative": {
|
|
38123
38156
|
message: {
|
|
38124
38157
|
fr: (p) => `Contenu g\xE9n\xE9r\xE9 en CSS porteur de texte (content: "${p.text}") : invisible pour les technologies d'assistance.`,
|
|
@@ -39586,6 +39619,20 @@ var rgaa_default = {
|
|
|
39586
39619
|
}
|
|
39587
39620
|
}
|
|
39588
39621
|
],
|
|
39622
|
+
overrides: {
|
|
39623
|
+
"presentational-element": {
|
|
39624
|
+
advisory: false,
|
|
39625
|
+
severity: "majeur"
|
|
39626
|
+
},
|
|
39627
|
+
"presentational-attribute": {
|
|
39628
|
+
advisory: false,
|
|
39629
|
+
severity: "majeur"
|
|
39630
|
+
},
|
|
39631
|
+
"presentational-spacing": {
|
|
39632
|
+
advisory: false,
|
|
39633
|
+
severity: "majeur"
|
|
39634
|
+
}
|
|
39635
|
+
},
|
|
39589
39636
|
themes: [
|
|
39590
39637
|
{
|
|
39591
39638
|
number: 1,
|
|
@@ -41567,7 +41614,7 @@ var rgaa_default = {
|
|
|
41567
41614
|
techniques: ["G140", "F32", "F33", "F34", "F48", "C6", "C8", "C18", "C22"],
|
|
41568
41615
|
wcag: ["1.3.1", "1.3.2"],
|
|
41569
41616
|
appliesTo: {
|
|
41570
|
-
ruleIds: []
|
|
41617
|
+
ruleIds: ["presentational-attribute", "presentational-element", "presentational-spacing"]
|
|
41571
41618
|
}
|
|
41572
41619
|
},
|
|
41573
41620
|
{
|
|
@@ -44125,6 +44172,30 @@ function keyValuePairs(doc) {
|
|
|
44125
44172
|
}
|
|
44126
44173
|
return out2;
|
|
44127
44174
|
}
|
|
44175
|
+
var PRESENTATIONAL_MARKUP_TAGS = /* @__PURE__ */ new Set(["basefont", "big", "blink", "center", "font", "marquee", "s", "strike", "tt"]);
|
|
44176
|
+
var PRESENTATIONAL_MARKUP_ATTRS = /* @__PURE__ */ new Set([
|
|
44177
|
+
"align",
|
|
44178
|
+
"alink",
|
|
44179
|
+
"background",
|
|
44180
|
+
"bgcolor",
|
|
44181
|
+
"border",
|
|
44182
|
+
"cellpadding",
|
|
44183
|
+
"cellspacing",
|
|
44184
|
+
"char",
|
|
44185
|
+
"charoff",
|
|
44186
|
+
"clear",
|
|
44187
|
+
"compact",
|
|
44188
|
+
"color",
|
|
44189
|
+
"frameborder",
|
|
44190
|
+
"hspace",
|
|
44191
|
+
"link",
|
|
44192
|
+
"marginheight",
|
|
44193
|
+
"marginwidth",
|
|
44194
|
+
"text",
|
|
44195
|
+
"valign",
|
|
44196
|
+
"vlink",
|
|
44197
|
+
"vspace"
|
|
44198
|
+
]);
|
|
44128
44199
|
var DOWNLOAD_HREF = /\.(pdf|docx?|xlsx?)(?:[?#]|$)/i;
|
|
44129
44200
|
var STATUS_CLASS = /(error|status|message|alert|notif|toast|feedback|live)/i;
|
|
44130
44201
|
var ROUTER_IMPORT = /['"](?:react-router(?:-dom)?|next\/(?:router|navigation)|vue-router|@remix-run\/[\w-]+|@tanstack\/[\w-]*router|@sveltejs\/kit|\$app\/(?:navigation|stores))['"]/;
|
|
@@ -44482,12 +44553,62 @@ var SUBJECTS = {
|
|
|
44482
44553
|
)
|
|
44483
44554
|
]),
|
|
44484
44555
|
// A change of context triggered by focus or by changing a value.
|
|
44556
|
+
// RGAA 7.4 / WCAG 3.2.1-3.2.2. The handlers are half the subject; the OTHER half is what a
|
|
44557
|
+
// script does once it runs, and that is where a context change actually happens: submitting a
|
|
44558
|
+
// form, navigating, opening a window. Looking only for onFocus/onBlur/onChange left the
|
|
44559
|
+
// criterion with an empty harvest on any codebase that binds its handlers in JavaScript
|
|
44560
|
+
// rather than in markup — which is most of them — and an empty harvest is a criterion the
|
|
44561
|
+
// adjudicator can only guess at.
|
|
44485
44562
|
contextChange: (docs) => docs.flatMap((d) => [
|
|
44486
44563
|
...linesOf(d, /\bon(?:Focus|Blur)\s*=/).map(
|
|
44487
44564
|
(l) => hAt(d, l.line, "handler", `focus handler \u2014 does it change context (navigate, submit, move focus)? ${l.text}`, `onfocus|${l.text}`)
|
|
44488
44565
|
),
|
|
44489
44566
|
...linesOf(d, /\bonChange\s*=/).map(
|
|
44490
44567
|
(l) => hAt(d, l.line, "handler", `change handler \u2014 does changing the value itself change context? ${l.text}`, `onchange|${l.text}`)
|
|
44568
|
+
),
|
|
44569
|
+
...linesOf(d, /\.submit\s*\(|\brequestSubmit\s*\(/).map(
|
|
44570
|
+
(l) => hAt(d, l.line, "submit", `script submits a form \u2014 is the user warned, or is it a button they pressed? ${l.text}`, `submit|${l.text}`)
|
|
44571
|
+
),
|
|
44572
|
+
...linesOf(d, /\blocation\s*\.\s*(?:href|assign|replace)\b|\blocation\s*=|\brouter\s*\.\s*(?:push|replace|navigate)\s*\(/).map(
|
|
44573
|
+
(l) => hAt(d, l.line, "navigate", `script navigates \u2014 is the change announced before it happens? ${l.text}`, `navigate|${l.text}`)
|
|
44574
|
+
),
|
|
44575
|
+
...linesOf(d, /\bwindow\s*\.\s*open\s*\(/).map(
|
|
44576
|
+
(l) => hAt(d, l.line, "open", `script opens a window \u2014 is it the user's action? ${l.text}`, `open|${l.text}`)
|
|
44577
|
+
)
|
|
44578
|
+
]),
|
|
44579
|
+
// RGAA 10.1 — the markup the standard forbids outright. The ENGINE decides this criterion
|
|
44580
|
+
// (src/rules/presentation.ts), so the adjudicator normally never sees it; this exists so the
|
|
44581
|
+
// criterion is never handed over with nothing to read, and so a reader asking « what is this
|
|
44582
|
+
// criterion about here? » gets the elements rather than the page's layout order.
|
|
44583
|
+
presentationalMarkup: (docs) => docs.flatMap(
|
|
44584
|
+
(d) => d.elements.filter((e) => PRESENTATIONAL_MARKUP_TAGS.has(e.tag) || Object.keys(e.attribs).some((a) => PRESENTATIONAL_MARKUP_ATTRS.has(a.toLowerCase()))).map(
|
|
44585
|
+
(e) => h(
|
|
44586
|
+
d,
|
|
44587
|
+
e,
|
|
44588
|
+
`presentational markup: <${e.tag}> \u2014 styling belongs in the stylesheet (RGAA tests 10.1.1 / 10.1.2)`,
|
|
44589
|
+
`presentational|${e.tag}|${Object.keys(e.attribs).sort().join(",")}`
|
|
44590
|
+
)
|
|
44591
|
+
)
|
|
44592
|
+
),
|
|
44593
|
+
// RGAA 13.2 — « l'ouverture d'une nouvelle fenêtre ne doit pas être déclenchée sans action de
|
|
44594
|
+
// l'utilisateur ». Its own subject, because inheriting `contextChange` through WCAG 3.2.1
|
|
44595
|
+
// handed it focus and change handlers: a real question, and a DIFFERENT one. Measured on run
|
|
44596
|
+
// 32508717451: 13.2 reached the adjudicator with nothing to read, the gate refused its `C`
|
|
44597
|
+
// twice for citing nothing, and it took a third pass to close.
|
|
44598
|
+
//
|
|
44599
|
+
// Three ways a window opens: a `target` that is not the current browsing context, a script
|
|
44600
|
+
// calling `window.open`, and a meta refresh. The first and third are markup; the second is a
|
|
44601
|
+
// literal API call that survives minification.
|
|
44602
|
+
newWindow: (docs) => docs.flatMap((d) => [
|
|
44603
|
+
...d.elements.filter((e) => {
|
|
44604
|
+
const t3 = (attr(e, "target") ?? "").toLowerCase();
|
|
44605
|
+
return t3 !== "" && t3 !== "_self" && t3 !== "_parent" && t3 !== "_top";
|
|
44606
|
+
}).map(
|
|
44607
|
+
(e) => h(d, e, `target="${attr(e, "target")}" \u2014 opens outside this context; is it the user's action, and is it announced?`, `target|${attr(e, "target")}`)
|
|
44608
|
+
),
|
|
44609
|
+
...d.elements.filter((e) => e.tag === "meta" && (attr(e, "http-equiv") ?? "").toLowerCase() === "refresh").map((e) => h(d, e, `meta refresh: content="${attr(e, "content")}" \u2014 a navigation nobody asked for`, `metarefresh|${attr(e, "content")}`)),
|
|
44610
|
+
...linesOf(d, /\bwindow\s*\.\s*open\s*\(/).map(
|
|
44611
|
+
(l) => hAt(d, l.line, "open", `window.open \u2014 is it reached only from a user action? ${l.text}`, `open|${l.text}`)
|
|
44491
44612
|
)
|
|
44492
44613
|
]),
|
|
44493
44614
|
// Reading order: anything that moves meaning-bearing content away from DOM order.
|
|
@@ -44766,7 +44887,13 @@ var PACK_SUBJECTS = {
|
|
|
44766
44887
|
"7.5": ["liveRegions"],
|
|
44767
44888
|
// Theme 10 — presentation. These ask what survives when CSS is off, what the stylesheet
|
|
44768
44889
|
// reorders, and what is hidden on purpose.
|
|
44769
|
-
|
|
44890
|
+
//
|
|
44891
|
+
// 10.1 is the exception, and it was mis-pointed: its three tests are the closed list of
|
|
44892
|
+
// forbidden presentational ELEMENTS and ATTRIBUTES plus spaces used as layout, which is
|
|
44893
|
+
// markup — not reading order. `readingOrder` harvested nothing for it, so the criterion
|
|
44894
|
+
// reached the adjudicator with an empty brief and could not be ruled on at all. The engine
|
|
44895
|
+
// decides it now (src/rules/presentation.ts); this names what a reader would look at.
|
|
44896
|
+
"10.1": ["presentationalMarkup"],
|
|
44770
44897
|
"10.2": ["hiddenContent", "structure"],
|
|
44771
44898
|
"10.3": ["readingOrder", "structure"],
|
|
44772
44899
|
"10.4": ["readingOrder"],
|
|
@@ -44779,6 +44906,9 @@ var PACK_SUBJECTS = {
|
|
|
44779
44906
|
"10.14": ["additionalContent", "focusables", "pointerHandlers"],
|
|
44780
44907
|
// Theme 13 — consultation.
|
|
44781
44908
|
"13.1": ["timers"],
|
|
44909
|
+
// 13.2 asks about a window OPENING, which is not the context-change question it inherited
|
|
44910
|
+
// from WCAG 3.2.1. See `newWindow`.
|
|
44911
|
+
"13.2": ["newWindow"],
|
|
44782
44912
|
"13.3": ["downloadDocs"],
|
|
44783
44913
|
"13.4": ["downloadDocs"],
|
|
44784
44914
|
"13.7": ["motion"],
|
|
@@ -44788,7 +44918,17 @@ var PACK_SUBJECTS = {
|
|
|
44788
44918
|
"13.11": ["pointerHandlers"]
|
|
44789
44919
|
}
|
|
44790
44920
|
};
|
|
44791
|
-
var EXISTENCE_SUBJECTS = /* @__PURE__ */ new Set([
|
|
44921
|
+
var EXISTENCE_SUBJECTS = /* @__PURE__ */ new Set([
|
|
44922
|
+
"images",
|
|
44923
|
+
"tables",
|
|
44924
|
+
"lists",
|
|
44925
|
+
"links",
|
|
44926
|
+
"controls",
|
|
44927
|
+
"autocomplete",
|
|
44928
|
+
"errors",
|
|
44929
|
+
"frames",
|
|
44930
|
+
"downloadDocs"
|
|
44931
|
+
]);
|
|
44792
44932
|
var subjectsForSc = (sc) => SC_SUBJECTS[sc] ?? [];
|
|
44793
44933
|
function subjectsForPackCriterion(standard, id, scs) {
|
|
44794
44934
|
const own = PACK_SUBJECTS[standard]?.[id];
|
|
@@ -47362,7 +47502,96 @@ var cssGeneratedContentInformative = {
|
|
|
47362
47502
|
return out2;
|
|
47363
47503
|
}
|
|
47364
47504
|
};
|
|
47365
|
-
var
|
|
47505
|
+
var PRESENTATIONAL_TAGS = /* @__PURE__ */ new Set(["basefont", "big", "blink", "center", "font", "marquee", "s", "strike", "tt"]);
|
|
47506
|
+
var PRESENTATIONAL_ATTRS = /* @__PURE__ */ new Set([
|
|
47507
|
+
"align",
|
|
47508
|
+
"alink",
|
|
47509
|
+
"background",
|
|
47510
|
+
"bgcolor",
|
|
47511
|
+
"border",
|
|
47512
|
+
"cellpadding",
|
|
47513
|
+
"cellspacing",
|
|
47514
|
+
"char",
|
|
47515
|
+
"charoff",
|
|
47516
|
+
"clear",
|
|
47517
|
+
"compact",
|
|
47518
|
+
"color",
|
|
47519
|
+
"frameborder",
|
|
47520
|
+
"hspace",
|
|
47521
|
+
"link",
|
|
47522
|
+
"marginheight",
|
|
47523
|
+
"marginwidth",
|
|
47524
|
+
"text",
|
|
47525
|
+
"valign",
|
|
47526
|
+
"vlink",
|
|
47527
|
+
"vspace"
|
|
47528
|
+
]);
|
|
47529
|
+
var DIMENSION_OK = /* @__PURE__ */ new Set(["img", "object", "embed", "canvas", "svg", "video", "iframe", "source", "input"]);
|
|
47530
|
+
var SIZE_OK = /* @__PURE__ */ new Set(["select"]);
|
|
47531
|
+
function intrinsic(el) {
|
|
47532
|
+
return el.tag === el.tag.toLowerCase() && !el.tag.includes("-");
|
|
47533
|
+
}
|
|
47534
|
+
function inSvg(el) {
|
|
47535
|
+
for (let p = el; p; p = p.parent) if (p.tag === "svg") return true;
|
|
47536
|
+
return false;
|
|
47537
|
+
}
|
|
47538
|
+
var presentationalElement = {
|
|
47539
|
+
id: "presentational-element",
|
|
47540
|
+
criteria: ["1.3.1"],
|
|
47541
|
+
severity: "majeur",
|
|
47542
|
+
advisory: true,
|
|
47543
|
+
run(doc) {
|
|
47544
|
+
return doc.elements.filter((el) => intrinsic(el) && PRESENTATIONAL_TAGS.has(el.tag)).map((el) => ({ criteriaId: "1.3.1", el, msgId: "presentational-element", params: { tag: el.tag } }));
|
|
47545
|
+
}
|
|
47546
|
+
};
|
|
47547
|
+
var presentationalAttribute = {
|
|
47548
|
+
id: "presentational-attribute",
|
|
47549
|
+
criteria: ["1.3.1"],
|
|
47550
|
+
severity: "majeur",
|
|
47551
|
+
advisory: true,
|
|
47552
|
+
run(doc) {
|
|
47553
|
+
const out2 = [];
|
|
47554
|
+
for (const el of doc.elements) {
|
|
47555
|
+
if (!intrinsic(el) || inSvg(el)) continue;
|
|
47556
|
+
for (const name2 of Object.keys(el.attribs)) {
|
|
47557
|
+
const n = name2.toLowerCase();
|
|
47558
|
+
const hit = PRESENTATIONAL_ATTRS.has(n) || (n === "width" || n === "height") && !DIMENSION_OK.has(el.tag) || n === "size" && !SIZE_OK.has(el.tag);
|
|
47559
|
+
if (!hit) continue;
|
|
47560
|
+
out2.push({ criteriaId: "1.3.1", el, msgId: "presentational-attribute", params: { attr: n, tag: el.tag } });
|
|
47561
|
+
break;
|
|
47562
|
+
}
|
|
47563
|
+
}
|
|
47564
|
+
return out2;
|
|
47565
|
+
}
|
|
47566
|
+
};
|
|
47567
|
+
var SPELLED_OUT = new RegExp("(?:\\p{L}[ \\u00a0]){4,}\\p{L}", "u");
|
|
47568
|
+
var NBSP_RUN = /\u00a0{3,}/u;
|
|
47569
|
+
var SPACING_EXEMPT = /* @__PURE__ */ new Set(["pre", "code", "kbd", "samp", "textarea", "script", "style", "svg"]);
|
|
47570
|
+
var presentationalSpacing = {
|
|
47571
|
+
id: "presentational-spacing",
|
|
47572
|
+
criteria: ["1.3.1"],
|
|
47573
|
+
severity: "majeur",
|
|
47574
|
+
advisory: true,
|
|
47575
|
+
run(doc) {
|
|
47576
|
+
const out2 = [];
|
|
47577
|
+
for (const el of doc.elements) {
|
|
47578
|
+
if (!intrinsic(el) || SPACING_EXEMPT.has(el.tag) || inSvg(el)) continue;
|
|
47579
|
+
const direct = el.children.filter((c2) => c2.type === "text").map((c2) => c2.data).join("");
|
|
47580
|
+
if (!direct.trim()) continue;
|
|
47581
|
+
const how = SPELLED_OUT.test(direct) ? "spelled-out" : NBSP_RUN.test(direct) ? "nbsp-run" : void 0;
|
|
47582
|
+
if (!how) continue;
|
|
47583
|
+
out2.push({ criteriaId: "1.3.1", el, msgId: "presentational-spacing", params: { how, tag: el.tag } });
|
|
47584
|
+
}
|
|
47585
|
+
return out2;
|
|
47586
|
+
}
|
|
47587
|
+
};
|
|
47588
|
+
var presentationRules = [
|
|
47589
|
+
metaViewportZoomBlock,
|
|
47590
|
+
cssGeneratedContentInformative,
|
|
47591
|
+
presentationalElement,
|
|
47592
|
+
presentationalAttribute,
|
|
47593
|
+
presentationalSpacing
|
|
47594
|
+
];
|
|
47366
47595
|
|
|
47367
47596
|
// src/rules/colors.ts
|
|
47368
47597
|
var SKIP_TAGS2 = /* @__PURE__ */ new Set(["script", "style", "head", "title", "meta", "noscript", "link"]);
|
|
@@ -53698,7 +53927,7 @@ function derivePackResults(audit2, packKey, pageId) {
|
|
|
53698
53927
|
return { id: pc.id, theme: pc.theme, status: "manual", findings, scs: pc.wcag };
|
|
53699
53928
|
}
|
|
53700
53929
|
const status = scResults.length ? aggregate2(scResults) : INAPPLICABLE_STATUS;
|
|
53701
|
-
if (status
|
|
53930
|
+
if (status !== "NC" && subjectAbsent(pc)) {
|
|
53702
53931
|
return { id: pc.id, theme: pc.theme, status: INAPPLICABLE_STATUS, findings, scs: pc.wcag, inapplicable: true };
|
|
53703
53932
|
}
|
|
53704
53933
|
const inapplicable = status === INAPPLICABLE_STATUS && (scResults.length === 0 || scResults.every((r) => r.inapplicable));
|
|
@@ -58721,7 +58950,16 @@ var T2 = {
|
|
|
58721
58950
|
briefContract: "> **CONTRAT DE VERDICT** \u2014 le pli est FERM\xC9 : un verdict auquel il manque son champ obligatoire est refus\xE9, et son crit\xE8re retourne \xAB \xE0 \xE9valuer \xBB en portant le refus. Renseignez le verdict de CE crit\xE8re, ici :",
|
|
58722
58951
|
absenceRule: "> **UNE NC EN FORME D'ABSENCE S'ANCRE QUAND M\xCAME.** \xAB Il n'y a pas de second syst\xE8me de navigation \xBB, \xAB il n'y a pas de moteur de recherche \xBB, \xAB aucun message d'erreur ne sugg\xE8re le format attendu \xBB : une absence se CONSTATE quelque part. Citez l'\xE9l\xE9ment et la page o\xF9 vous l'avez constat\xE9e \u2014 le `<nav>`, le `<header>`, le formulaire \u2014 avec son `file`, sa `line` et son `snippet`. Une NC sans `file` est refus\xE9e aussi s\xFBrement qu'un `C` sans citations. Et si le sujet du crit\xE8re n'existe nulle part dans le p\xE9rim\xE8tre audit\xE9, le verdict n'est pas `NC` : c'est `NA`, avec sa justification.",
|
|
58723
58952
|
incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
|
|
58724
|
-
|
|
58953
|
+
// WHAT AN EMPTY HARVEST MAY BE ANSWERED WITH, said outright.
|
|
58954
|
+
//
|
|
58955
|
+
// It used to read « décidez depuis la source, ou laissez `manual` avec une raison », which
|
|
58956
|
+
// invites the one answer the gate always refuses: a `C` with nothing to cite. Measured on
|
|
58957
|
+
// run 32508717451 (Sonnet, RGAA, 3 passes): 10.1 and 13.2 both arrived with an empty
|
|
58958
|
+
// harvest, both were ruled `C`, and the gate refused them — twice for 13.2, three times for
|
|
58959
|
+
// 10.1, which never closed. The model was not being careless; it was told to decide, and it
|
|
58960
|
+
// decided. So the two answers that ARE accepted are named, and the difference between them
|
|
58961
|
+
// is spelled out, because it is a real one.
|
|
58962
|
+
none: "**AUCUNE \xC9VIDENCE MOISSONN\xC9E POUR CE CRIT\xC8RE.** Un `C` sera REFUS\xC9 ici quelle que soit la justification : la porte exige au moins une citation, et il n'y a rien \xE0 citer. Deux r\xE9ponses sont accept\xE9es, et elles ne disent pas la m\xEAme chose \u2014 `NA` : rien dans le p\xE9rim\xE8tre audit\xE9 n'est concern\xE9 (aucun script, aucun document, aucun composant de ce type) ; `manual` avec `reason: \"undecidable\"` : le sujet existe peut-\xEAtre, mais rien ici ne permet de le voir.",
|
|
58725
58963
|
questions: "\xC0 v\xE9rifier manuellement",
|
|
58726
58964
|
decide: "R\xE8gle de d\xE9cision",
|
|
58727
58965
|
na: "Non applicable si",
|
|
@@ -58755,7 +58993,7 @@ var T2 = {
|
|
|
58755
58993
|
briefContract: "> **VERDICT CONTRACT** \u2014 the fold is FAIL-CLOSED: a verdict missing its required field is refused, and its criterion goes back to \xAB to assess \xBB carrying the refusal. Record THIS criterion's verdict, here:",
|
|
58756
58994
|
absenceRule: "> **AN NC SHAPED LIKE AN ABSENCE STILL HAS TO BE ANCHORED.** \xAB There is no second navigation system \xBB, \xAB there is no search engine \xBB, \xAB no error message suggests the expected format \xBB: an absence is OBSERVED somewhere. Cite the element and the page you observed it on \u2014 the `<nav>`, the `<header>`, the form \u2014 with its `file`, `line` and `snippet`. An NC with no `file` is refused exactly as surely as a `C` with no citations. And when the criterion's subject exists nowhere in the audited scope, the verdict is not `NC`: it is `NA`, with its justification.",
|
|
58757
58995
|
incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
|
|
58758
|
-
none:
|
|
58996
|
+
none: '**NO EVIDENCE WAS HARVESTED FOR THIS CRITERION.** A `C` will be REFUSED here however good the justification: the gate requires at least one citation and there is nothing to cite. Two answers are accepted, and they say different things \u2014 `NA`: nothing in the audited scope is concerned (no script, no document, no component of that kind); `manual` with `reason: "undecidable"`: the subject may exist, but nothing here lets you see it.',
|
|
58759
58997
|
questions: "To verify manually",
|
|
58760
58998
|
decide: "Decision rule",
|
|
58761
58999
|
na: "Not applicable when",
|
|
@@ -5,7 +5,7 @@ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should
|
|
|
5
5
|
allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
|
|
6
6
|
license: MIT
|
|
7
7
|
metadata:
|
|
8
|
-
version: 5.
|
|
8
|
+
version: 5.24.0
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|