ultra11y 5.4.0 → 5.4.1
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/package.json +1 -1
- package/scripts/ultra11y.mjs +14 -3
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +14 -3
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +14 -3
- package/src/audit.ts +47 -7
- 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.4.
|
|
13
|
+
"version": "5.4.1",
|
|
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.
|
|
4
|
+
"version": "5.4.1",
|
|
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.
|
|
4
|
+
"version": "5.4.1",
|
|
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.4.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.4.1";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.1",
|
|
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",
|
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.4.
|
|
9
|
+
var VERSION = "5.4.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -47435,8 +47435,19 @@ var SUBJECT_MATTER = {
|
|
|
47435
47435
|
// Identify Input Purpose — about autocomplete on fields collecting user data.
|
|
47436
47436
|
"1.3.5": hasFormControl
|
|
47437
47437
|
};
|
|
47438
|
+
var MEDIA_HINT = /(youtube|youtu\.be|vimeo|dailymotion|soundcloud|spotify|twitch|wistia|brightcove|jwplayer|videopress|podcast|player|vid[eé]o|audio|baladodiffusion)/i;
|
|
47439
|
+
var MEDIA_EXT = /\.(mp4|webm|ogv|ogg|mov|m4v|avi|mpe?g|mp3|wav|flac|m4a|aac|opus|m3u8|mpd)(?:[?#]|$)/i;
|
|
47438
47440
|
function hasMedia(d) {
|
|
47439
|
-
|
|
47441
|
+
if (has(d, "audio", "video", "track", "source")) return true;
|
|
47442
|
+
return d.elements.some((el) => {
|
|
47443
|
+
if (el.tag === "object" || el.tag === "embed") {
|
|
47444
|
+
const type = (el.attribs.type ?? "").toLowerCase();
|
|
47445
|
+
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/") ? true : false;
|
|
47446
|
+
}
|
|
47447
|
+
if (el.tag !== "iframe") return false;
|
|
47448
|
+
const hay = `${el.attribs.src ?? ""} ${el.attribs.title ?? ""} ${el.attribs.allow ?? ""}`;
|
|
47449
|
+
return MEDIA_HINT.test(hay) || MEDIA_EXT.test(el.attribs.src ?? "");
|
|
47450
|
+
});
|
|
47440
47451
|
}
|
|
47441
47452
|
function hasMotionApi(d) {
|
|
47442
47453
|
return /\b(?:devicemotion|deviceorientation|DeviceMotionEvent|DeviceOrientationEvent|Accelerometer|Gyroscope|requestPermission)\b/i.test(d.source);
|
|
@@ -47480,7 +47491,7 @@ function residualReason(automatability2, sc) {
|
|
|
47480
47491
|
function subjectMatterReason(sc, files) {
|
|
47481
47492
|
const scope = `nothing in the ${files} file(s) audited is concerned`;
|
|
47482
47493
|
if (sc.startsWith("1.2."))
|
|
47483
|
-
return `No time-based media in scope: ${scope} \u2014 no <audio>, <video>, <track
|
|
47494
|
+
return `No time-based media in scope: ${scope} \u2014 no <audio>, <video>, <track> or <source> element, no <object>/<embed>, and no <iframe> pointing at media.`;
|
|
47484
47495
|
if (sc === "2.5.4") return `No motion actuation in scope: ${scope} \u2014 no device-motion or device-orientation API is used.`;
|
|
47485
47496
|
if (sc === "1.3.5" || sc.startsWith("3.3.")) return `No user input in scope: ${scope} \u2014 no form control (native, ARIA or contenteditable) was found.`;
|
|
47486
47497
|
return `No element in scope is concerned by this success criterion (${scope}).`;
|
|
@@ -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.4.
|
|
8
|
+
version: 5.4.1
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -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.4.
|
|
9
|
+
var VERSION = "5.4.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -47435,8 +47435,19 @@ var SUBJECT_MATTER = {
|
|
|
47435
47435
|
// Identify Input Purpose — about autocomplete on fields collecting user data.
|
|
47436
47436
|
"1.3.5": hasFormControl
|
|
47437
47437
|
};
|
|
47438
|
+
var MEDIA_HINT = /(youtube|youtu\.be|vimeo|dailymotion|soundcloud|spotify|twitch|wistia|brightcove|jwplayer|videopress|podcast|player|vid[eé]o|audio|baladodiffusion)/i;
|
|
47439
|
+
var MEDIA_EXT = /\.(mp4|webm|ogv|ogg|mov|m4v|avi|mpe?g|mp3|wav|flac|m4a|aac|opus|m3u8|mpd)(?:[?#]|$)/i;
|
|
47438
47440
|
function hasMedia(d) {
|
|
47439
|
-
|
|
47441
|
+
if (has(d, "audio", "video", "track", "source")) return true;
|
|
47442
|
+
return d.elements.some((el) => {
|
|
47443
|
+
if (el.tag === "object" || el.tag === "embed") {
|
|
47444
|
+
const type = (el.attribs.type ?? "").toLowerCase();
|
|
47445
|
+
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/") ? true : false;
|
|
47446
|
+
}
|
|
47447
|
+
if (el.tag !== "iframe") return false;
|
|
47448
|
+
const hay = `${el.attribs.src ?? ""} ${el.attribs.title ?? ""} ${el.attribs.allow ?? ""}`;
|
|
47449
|
+
return MEDIA_HINT.test(hay) || MEDIA_EXT.test(el.attribs.src ?? "");
|
|
47450
|
+
});
|
|
47440
47451
|
}
|
|
47441
47452
|
function hasMotionApi(d) {
|
|
47442
47453
|
return /\b(?:devicemotion|deviceorientation|DeviceMotionEvent|DeviceOrientationEvent|Accelerometer|Gyroscope|requestPermission)\b/i.test(d.source);
|
|
@@ -47480,7 +47491,7 @@ function residualReason(automatability2, sc) {
|
|
|
47480
47491
|
function subjectMatterReason(sc, files) {
|
|
47481
47492
|
const scope = `nothing in the ${files} file(s) audited is concerned`;
|
|
47482
47493
|
if (sc.startsWith("1.2."))
|
|
47483
|
-
return `No time-based media in scope: ${scope} \u2014 no <audio>, <video>, <track
|
|
47494
|
+
return `No time-based media in scope: ${scope} \u2014 no <audio>, <video>, <track> or <source> element, no <object>/<embed>, and no <iframe> pointing at media.`;
|
|
47484
47495
|
if (sc === "2.5.4") return `No motion actuation in scope: ${scope} \u2014 no device-motion or device-orientation API is used.`;
|
|
47485
47496
|
if (sc === "1.3.5" || sc.startsWith("3.3.")) return `No user input in scope: ${scope} \u2014 no form control (native, ARIA or contenteditable) was found.`;
|
|
47486
47497
|
return `No element in scope is concerned by this success criterion (${scope}).`;
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibili
|
|
|
4
4
|
when_to_use: "Invoke when the user asks for an accessibility AUDIT or a formal deliverable: audit a repo, site or page against WCAG 2.2 AA or a country standard (RGAA, Section 508, EN 301 549); produce a dated conformance report, a criterion grid, a PRD backlog, or tickets (GitHub/GitLab/Jira); author accessible markup from scratch; wire the repo gate (init --hook/--ci) or a rendered-DOM capture pipeline. For a review of the code UNDER CHANGE (diff, branch, PR), use the `review-a11y` skill instead."
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: 5.4.
|
|
7
|
+
version: 5.4.1
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -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.4.
|
|
9
|
+
var VERSION = "5.4.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -47435,8 +47435,19 @@ var SUBJECT_MATTER = {
|
|
|
47435
47435
|
// Identify Input Purpose — about autocomplete on fields collecting user data.
|
|
47436
47436
|
"1.3.5": hasFormControl
|
|
47437
47437
|
};
|
|
47438
|
+
var MEDIA_HINT = /(youtube|youtu\.be|vimeo|dailymotion|soundcloud|spotify|twitch|wistia|brightcove|jwplayer|videopress|podcast|player|vid[eé]o|audio|baladodiffusion)/i;
|
|
47439
|
+
var MEDIA_EXT = /\.(mp4|webm|ogv|ogg|mov|m4v|avi|mpe?g|mp3|wav|flac|m4a|aac|opus|m3u8|mpd)(?:[?#]|$)/i;
|
|
47438
47440
|
function hasMedia(d) {
|
|
47439
|
-
|
|
47441
|
+
if (has(d, "audio", "video", "track", "source")) return true;
|
|
47442
|
+
return d.elements.some((el) => {
|
|
47443
|
+
if (el.tag === "object" || el.tag === "embed") {
|
|
47444
|
+
const type = (el.attribs.type ?? "").toLowerCase();
|
|
47445
|
+
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/") ? true : false;
|
|
47446
|
+
}
|
|
47447
|
+
if (el.tag !== "iframe") return false;
|
|
47448
|
+
const hay = `${el.attribs.src ?? ""} ${el.attribs.title ?? ""} ${el.attribs.allow ?? ""}`;
|
|
47449
|
+
return MEDIA_HINT.test(hay) || MEDIA_EXT.test(el.attribs.src ?? "");
|
|
47450
|
+
});
|
|
47440
47451
|
}
|
|
47441
47452
|
function hasMotionApi(d) {
|
|
47442
47453
|
return /\b(?:devicemotion|deviceorientation|DeviceMotionEvent|DeviceOrientationEvent|Accelerometer|Gyroscope|requestPermission)\b/i.test(d.source);
|
|
@@ -47480,7 +47491,7 @@ function residualReason(automatability2, sc) {
|
|
|
47480
47491
|
function subjectMatterReason(sc, files) {
|
|
47481
47492
|
const scope = `nothing in the ${files} file(s) audited is concerned`;
|
|
47482
47493
|
if (sc.startsWith("1.2."))
|
|
47483
|
-
return `No time-based media in scope: ${scope} \u2014 no <audio>, <video>, <track
|
|
47494
|
+
return `No time-based media in scope: ${scope} \u2014 no <audio>, <video>, <track> or <source> element, no <object>/<embed>, and no <iframe> pointing at media.`;
|
|
47484
47495
|
if (sc === "2.5.4") return `No motion actuation in scope: ${scope} \u2014 no device-motion or device-orientation API is used.`;
|
|
47485
47496
|
if (sc === "1.3.5" || sc.startsWith("3.3.")) return `No user input in scope: ${scope} \u2014 no form control (native, ARIA or contenteditable) was found.`;
|
|
47486
47497
|
return `No element in scope is concerned by this success criterion (${scope}).`;
|
package/src/audit.ts
CHANGED
|
@@ -81,10 +81,12 @@ const APPLICABLE: Record<string, (d: Doc) => boolean> = {
|
|
|
81
81
|
// Three rules keep this honest, because a wrong NA is far worse than an honest « to assess » —
|
|
82
82
|
// it is a non-conformity hidden inside a report someone signs:
|
|
83
83
|
//
|
|
84
|
-
// 1.
|
|
85
|
-
// possibly apply here?", so it
|
|
86
|
-
//
|
|
87
|
-
//
|
|
84
|
+
// 1. UNCERTAINTY RESOLVES TOWARDS "APPLICABLE". A predicate answers "could this criterion
|
|
85
|
+
// possibly apply here?", so it says YES whenever it genuinely cannot tell — an `<object>`
|
|
86
|
+
// with no declared `type`, for instance. That is not the same as saying yes to everything:
|
|
87
|
+
// a predicate that treats every embed as a video is not cautious, it is wrong, and it
|
|
88
|
+
// costs a human the work of re-deriving "nothing here". Measured: one analytics `<iframe>`
|
|
89
|
+
// once held all twelve RGAA multimedia criteria open on a codebase with no media at all.
|
|
88
90
|
// 2. APPLICABILITY IS OR-FOLDED ACROSS THE WHOLE SCOPE (see `foldDoc`). One document with a
|
|
89
91
|
// `<video>` keeps the media criteria open for the entire audit. NA means "nothing,
|
|
90
92
|
// anywhere in what was audited" — never "nothing in this file".
|
|
@@ -119,9 +121,47 @@ const SUBJECT_MATTER: Record<string, (d: Doc) => boolean> = {
|
|
|
119
121
|
"1.3.5": hasFormControl,
|
|
120
122
|
};
|
|
121
123
|
|
|
122
|
-
/**
|
|
124
|
+
/** A URL or MIME type that looks like time-based media, or a player embedding one. */
|
|
125
|
+
// Accented forms included on purpose: the standards this serves are read in French, so a
|
|
126
|
+
// `title="Vidéo de présentation"` must count. `\bvideo\b` did not match it — the boundary falls
|
|
127
|
+
// inside the word — and an accent is the last thing that should decide whether a criterion is
|
|
128
|
+
// assessed.
|
|
129
|
+
const MEDIA_HINT =
|
|
130
|
+
/(youtube|youtu\.be|vimeo|dailymotion|soundcloud|spotify|twitch|wistia|brightcove|jwplayer|videopress|podcast|player|vid[eé]o|audio|baladodiffusion)/i;
|
|
131
|
+
const MEDIA_EXT = /\.(mp4|webm|ogv|ogg|mov|m4v|avi|mpe?g|mp3|wav|flac|m4a|aac|opus|m3u8|mpd)(?:[?#]|$)/i;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Is there time-based media — the subject of WCAG 1.2.x — anywhere in this document?
|
|
135
|
+
*
|
|
136
|
+
* Split in two, because "over-inclusion is the safe direction" is a rule about UNCERTAINTY, not
|
|
137
|
+
* a licence to treat every embed as a video. Measured on a real audit: one `<iframe>` holding an
|
|
138
|
+
* analytics opt-out widget kept all 12 RGAA multimedia criteria « to assess » across a codebase
|
|
139
|
+
* with no audio and no video anywhere. That is not caution, it is a false positive with a cost —
|
|
140
|
+
* twelve criteria a human now has to read through to reach the same "nothing here" the engine
|
|
141
|
+
* could have proved.
|
|
142
|
+
*
|
|
143
|
+
* So:
|
|
144
|
+
* - `<video>`, `<audio>`, `<track>`, `<source>` are media, full stop.
|
|
145
|
+
* - `<object>`/`<embed>` are media unless their `type` says otherwise — an unknown type stays
|
|
146
|
+
* applicable, since that is genuine uncertainty.
|
|
147
|
+
* - an `<iframe>` counts only when its `src`/`title` actually points at media (a player host,
|
|
148
|
+
* a media extension, the words video/audio/podcast). A widget iframe does not.
|
|
149
|
+
* - `<canvas>` and `<marquee>` are NOT here. Neither can carry a caption or an audio
|
|
150
|
+
* description, which is what 1.2.x is about; moving content is 2.2.2 and flashing is 2.3.1,
|
|
151
|
+
* and those have their own criteria.
|
|
152
|
+
*/
|
|
123
153
|
function hasMedia(d: Doc): boolean {
|
|
124
|
-
|
|
154
|
+
if (has(d, "audio", "video", "track", "source")) return true;
|
|
155
|
+
return d.elements.some((el) => {
|
|
156
|
+
if (el.tag === "object" || el.tag === "embed") {
|
|
157
|
+
const type = (el.attribs.type ?? "").toLowerCase();
|
|
158
|
+
// A declared non-media type rules it out; anything else is uncertain, so it counts.
|
|
159
|
+
return !type || type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("application/") ? true : false;
|
|
160
|
+
}
|
|
161
|
+
if (el.tag !== "iframe") return false;
|
|
162
|
+
const hay = `${el.attribs.src ?? ""} ${el.attribs.title ?? ""} ${el.attribs.allow ?? ""}`;
|
|
163
|
+
return MEDIA_HINT.test(hay) || MEDIA_EXT.test(el.attribs.src ?? "");
|
|
164
|
+
});
|
|
125
165
|
}
|
|
126
166
|
|
|
127
167
|
/** Device-motion input: the only thing WCAG 2.5.4 is about. */
|
|
@@ -194,7 +234,7 @@ function residualReason(automatability: string, sc?: string): string {
|
|
|
194
234
|
function subjectMatterReason(sc: string, files: number): string {
|
|
195
235
|
const scope = `nothing in the ${files} file(s) audited is concerned`;
|
|
196
236
|
if (sc.startsWith("1.2."))
|
|
197
|
-
return `No time-based media in scope: ${scope} — no <audio>, <video>, <track
|
|
237
|
+
return `No time-based media in scope: ${scope} — no <audio>, <video>, <track> or <source> element, no <object>/<embed>, and no <iframe> pointing at media.`;
|
|
198
238
|
if (sc === "2.5.4") return `No motion actuation in scope: ${scope} — no device-motion or device-orientation API is used.`;
|
|
199
239
|
if (sc === "1.3.5" || sc.startsWith("3.3.")) return `No user input in scope: ${scope} — no form control (native, ARIA or contenteditable) was found.`;
|
|
200
240
|
return `No element in scope is concerned by this success criterion (${scope}).`;
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Single source of truth for shared types + the embedded version. sync-version.mjs
|
|
2
2
|
// rewrites VERSION at release time (kept in lockstep with package.json + SKILL.md).
|
|
3
|
-
export const VERSION = "5.4.
|
|
3
|
+
export const VERSION = "5.4.1";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|