ultra11y 5.4.1 → 5.5.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.
@@ -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.1",
13
+ "version": "5.5.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.1",
4
+ "version": "5.5.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.1",
4
+ "version": "5.5.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.1";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.5.1";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
package/dist/cypress.mjs CHANGED
@@ -65,19 +65,36 @@ var COLLECT_SNAPSHOT = `(() => {
65
65
  // query that locks the orientation (1.3.4) \u2014 and this is the only place they exist.
66
66
  // A cross-origin sheet throws on .cssRules: COUNT those instead of ignoring them, so a rule
67
67
  // reading this digest can tell "nothing there" apart from "I could not look".
68
+ // The cap is on the ORDINARY pool only, and the traversal never stops early.
69
+ //
70
+ // It used to return the moment the pool filled, which abandoned every remaining sheet \u2014
71
+ // and on a design-system app the pool fills long before the end. Measured on a real capture
72
+ // of 38 pages: all 38 came back truncated, so rendered-focus-not-visible declined on every
73
+ // one of them and 2.4.7 could never be decided by any number of scans. The rule was right to
74
+ // decline (a :focus rule might have been among the dropped ones); the collector was wrong to
75
+ // make that unavoidable.
76
+ //
77
+ // So the rules a rendered criterion actually READS \u2014 :focus styling, orientation locks,
78
+ // pinned positioning, animation \u2014 are kept whatever the pool is doing, and truncated now
79
+ // means "some ordinary rule was dropped", which is a much weaker statement. A rule can then
80
+ // ask for what it needs instead of refusing on a cap that never concerned it.
68
81
  const cssRules = [];
69
82
  let unreadable = 0;
83
+ let dropped = 0;
70
84
  const MAXR = 4000;
71
85
  const camel = (p) => p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
86
+ const KEEP_SELECTOR = /:focus|:target|:active/i;
87
+ const KEEP_PROP = /^(?:outline|position|animation|transition|transform)/;
72
88
  const walk = (list, media) => {
73
89
  for (const r of list) {
74
- if (cssRules.length >= MAXR) return;
75
90
  if (typeof r.selectorText === 'string' && r.style) {
76
91
  const decls = {};
77
92
  for (let i = 0; i < r.style.length; i++) {
78
93
  const prop = r.style[i];
79
94
  decls[camel(prop)] = r.style.getPropertyValue(prop);
80
95
  }
96
+ const keep = KEEP_SELECTOR.test(r.selectorText) || Object.keys(decls).some((k) => KEEP_PROP.test(k));
97
+ if (cssRules.length >= MAXR && !keep) { dropped++; continue; }
81
98
  cssRules.push(media ? { selector: r.selectorText, media: media, decls: decls } : { selector: r.selectorText, decls: decls });
82
99
  } else if (r.cssRules) {
83
100
  const cond = r.conditionText || (r.media && r.media.mediaText) || media;
@@ -96,8 +113,12 @@ var COLLECT_SNAPSHOT = `(() => {
96
113
  const truncated = els.length > MAX;
97
114
  return {
98
115
  dom: document.documentElement.outerHTML,
99
- css: { v: 1, rules: cssRules, unreadable: unreadable, truncated: cssRules.length >= MAXR },
116
+ css: { v: 1, rules: cssRules, unreadable: unreadable, truncated: dropped > 0, dropped: dropped },
100
117
  title: document.title,
118
+ // The doctype is NOT part of documentElement.outerHTML, so a capture that records only
119
+ // the DOM drops it \u2014 and RGAA 8.1 (is a doctype present, valid, and before <html>?) then
120
+ // has nothing to look at. Recorded as its own field for that reason.
121
+ doctype: document.doctype ? '<!DOCTYPE ' + document.doctype.name + (document.doctype.publicId ? ' PUBLIC "' + document.doctype.publicId + '"' : '') + (document.doctype.systemId ? ' "' + document.doctype.systemId + '"' : '') + '>' : '',
101
122
  lang: document.documentElement.getAttribute('lang') || '',
102
123
  url: location.href,
103
124
  viewport: { width: window.innerWidth, height: window.innerHeight },
@@ -70,19 +70,36 @@ var COLLECT_SNAPSHOT = `(() => {
70
70
  // query that locks the orientation (1.3.4) \u2014 and this is the only place they exist.
71
71
  // A cross-origin sheet throws on .cssRules: COUNT those instead of ignoring them, so a rule
72
72
  // reading this digest can tell "nothing there" apart from "I could not look".
73
+ // The cap is on the ORDINARY pool only, and the traversal never stops early.
74
+ //
75
+ // It used to return the moment the pool filled, which abandoned every remaining sheet \u2014
76
+ // and on a design-system app the pool fills long before the end. Measured on a real capture
77
+ // of 38 pages: all 38 came back truncated, so rendered-focus-not-visible declined on every
78
+ // one of them and 2.4.7 could never be decided by any number of scans. The rule was right to
79
+ // decline (a :focus rule might have been among the dropped ones); the collector was wrong to
80
+ // make that unavoidable.
81
+ //
82
+ // So the rules a rendered criterion actually READS \u2014 :focus styling, orientation locks,
83
+ // pinned positioning, animation \u2014 are kept whatever the pool is doing, and truncated now
84
+ // means "some ordinary rule was dropped", which is a much weaker statement. A rule can then
85
+ // ask for what it needs instead of refusing on a cap that never concerned it.
73
86
  const cssRules = [];
74
87
  let unreadable = 0;
88
+ let dropped = 0;
75
89
  const MAXR = 4000;
76
90
  const camel = (p) => p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
91
+ const KEEP_SELECTOR = /:focus|:target|:active/i;
92
+ const KEEP_PROP = /^(?:outline|position|animation|transition|transform)/;
77
93
  const walk = (list, media) => {
78
94
  for (const r of list) {
79
- if (cssRules.length >= MAXR) return;
80
95
  if (typeof r.selectorText === 'string' && r.style) {
81
96
  const decls = {};
82
97
  for (let i = 0; i < r.style.length; i++) {
83
98
  const prop = r.style[i];
84
99
  decls[camel(prop)] = r.style.getPropertyValue(prop);
85
100
  }
101
+ const keep = KEEP_SELECTOR.test(r.selectorText) || Object.keys(decls).some((k) => KEEP_PROP.test(k));
102
+ if (cssRules.length >= MAXR && !keep) { dropped++; continue; }
86
103
  cssRules.push(media ? { selector: r.selectorText, media: media, decls: decls } : { selector: r.selectorText, decls: decls });
87
104
  } else if (r.cssRules) {
88
105
  const cond = r.conditionText || (r.media && r.media.mediaText) || media;
@@ -101,8 +118,12 @@ var COLLECT_SNAPSHOT = `(() => {
101
118
  const truncated = els.length > MAX;
102
119
  return {
103
120
  dom: document.documentElement.outerHTML,
104
- css: { v: 1, rules: cssRules, unreadable: unreadable, truncated: cssRules.length >= MAXR },
121
+ css: { v: 1, rules: cssRules, unreadable: unreadable, truncated: dropped > 0, dropped: dropped },
105
122
  title: document.title,
123
+ // The doctype is NOT part of documentElement.outerHTML, so a capture that records only
124
+ // the DOM drops it \u2014 and RGAA 8.1 (is a doctype present, valid, and before <html>?) then
125
+ // has nothing to look at. Recorded as its own field for that reason.
126
+ doctype: document.doctype ? '<!DOCTYPE ' + document.doctype.name + (document.doctype.publicId ? ' PUBLIC "' + document.doctype.publicId + '"' : '') + (document.doctype.systemId ? ' "' + document.doctype.systemId + '"' : '') + '>' : '',
106
127
  lang: document.documentElement.getAttribute('lang') || '',
107
128
  url: location.href,
108
129
  viewport: { width: window.innerWidth, height: window.innerHeight },
package/docker/runner.mjs CHANGED
@@ -3,7 +3,7 @@ import axe from "axe-core";
3
3
  const target = process.argv[2];
4
4
  const isFile = target.startsWith("/work/");
5
5
  const SNAPSHOT = process.env.ULTRA11Y_SNAPSHOT !== "0";
6
- const COLLECT = "(() => {\n const PROPS = [\"color\",\"backgroundColor\",\"backgroundImage\",\"fontSize\",\"fontWeight\",\"fontStyle\",\"textDecorationLine\",\"textTransform\",\"lineHeight\",\"letterSpacing\",\"wordSpacing\",\"whiteSpace\",\"display\",\"visibility\",\"opacity\",\"position\",\"overflowX\",\"overflowY\",\"outlineStyle\",\"outlineWidth\",\"outlineColor\",\"borderTopStyle\",\"borderRightStyle\",\"borderBottomStyle\",\"borderLeftStyle\",\"borderTopWidth\",\"borderRightWidth\",\"borderBottomWidth\",\"borderLeftWidth\",\"borderTopColor\",\"borderRightColor\",\"borderBottomColor\",\"borderLeftColor\",\"boxShadow\",\"cursor\"];\n const MAX = 5000;\n const els = document.querySelectorAll('*');\n const styles = [];\n const boxes = [];\n const n = Math.min(els.length, MAX);\n for (let i = 0; i < n; i++) {\n const el = els[i];\n const tag = el.tagName.toLowerCase();\n const cs = getComputedStyle(el);\n const css = {};\n for (const p of PROPS) {\n const v = cs[p];\n if (v !== undefined && v !== null && v !== '') css[p] = String(v);\n }\n styles.push({ i: i, tag: tag, css: css });\n const r = el.getBoundingClientRect();\n 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) });\n }\n // The page's own stylesheets. Some criteria are properties of the STYLESHEET rather than of\n // any element's computed style — focus styling removed with no replacement (2.4.7), a media\n // query that locks the orientation (1.3.4) — and this is the only place they exist.\n // A cross-origin sheet throws on .cssRules: COUNT those instead of ignoring them, so a rule\n // reading this digest can tell \"nothing there\" apart from \"I could not look\".\n const cssRules = [];\n let unreadable = 0;\n const MAXR = 4000;\n const camel = (p) => p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());\n const walk = (list, media) => {\n for (const r of list) {\n if (cssRules.length >= MAXR) return;\n if (typeof r.selectorText === 'string' && r.style) {\n const decls = {};\n for (let i = 0; i < r.style.length; i++) {\n const prop = r.style[i];\n decls[camel(prop)] = r.style.getPropertyValue(prop);\n }\n cssRules.push(media ? { selector: r.selectorText, media: media, decls: decls } : { selector: r.selectorText, decls: decls });\n } else if (r.cssRules) {\n const cond = r.conditionText || (r.media && r.media.mediaText) || media;\n walk(r.cssRules, cond);\n }\n }\n };\n for (const sheet of document.styleSheets) {\n try {\n walk(sheet.cssRules, undefined);\n } catch (e) {\n unreadable++;\n }\n }\n\n const truncated = els.length > MAX;\n return {\n dom: document.documentElement.outerHTML,\n css: { v: 1, rules: cssRules, unreadable: unreadable, truncated: cssRules.length >= MAXR },\n title: document.title,\n lang: document.documentElement.getAttribute('lang') || '',\n url: location.href,\n viewport: { width: window.innerWidth, height: window.innerHeight },\n styles: { v: 1, entries: styles, truncated: truncated },\n boxes: { v: 1, entries: boxes, truncated: truncated }\n };\n})()";
6
+ const COLLECT = "(() => {\n const PROPS = [\"color\",\"backgroundColor\",\"backgroundImage\",\"fontSize\",\"fontWeight\",\"fontStyle\",\"textDecorationLine\",\"textTransform\",\"lineHeight\",\"letterSpacing\",\"wordSpacing\",\"whiteSpace\",\"display\",\"visibility\",\"opacity\",\"position\",\"overflowX\",\"overflowY\",\"outlineStyle\",\"outlineWidth\",\"outlineColor\",\"borderTopStyle\",\"borderRightStyle\",\"borderBottomStyle\",\"borderLeftStyle\",\"borderTopWidth\",\"borderRightWidth\",\"borderBottomWidth\",\"borderLeftWidth\",\"borderTopColor\",\"borderRightColor\",\"borderBottomColor\",\"borderLeftColor\",\"boxShadow\",\"cursor\"];\n const MAX = 5000;\n const els = document.querySelectorAll('*');\n const styles = [];\n const boxes = [];\n const n = Math.min(els.length, MAX);\n for (let i = 0; i < n; i++) {\n const el = els[i];\n const tag = el.tagName.toLowerCase();\n const cs = getComputedStyle(el);\n const css = {};\n for (const p of PROPS) {\n const v = cs[p];\n if (v !== undefined && v !== null && v !== '') css[p] = String(v);\n }\n styles.push({ i: i, tag: tag, css: css });\n const r = el.getBoundingClientRect();\n 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) });\n }\n // The page's own stylesheets. Some criteria are properties of the STYLESHEET rather than of\n // any element's computed style — focus styling removed with no replacement (2.4.7), a media\n // query that locks the orientation (1.3.4) — and this is the only place they exist.\n // A cross-origin sheet throws on .cssRules: COUNT those instead of ignoring them, so a rule\n // reading this digest can tell \"nothing there\" apart from \"I could not look\".\n // The cap is on the ORDINARY pool only, and the traversal never stops early.\n //\n // It used to return the moment the pool filled, which abandoned every remaining sheet —\n // and on a design-system app the pool fills long before the end. Measured on a real capture\n // of 38 pages: all 38 came back truncated, so rendered-focus-not-visible declined on every\n // one of them and 2.4.7 could never be decided by any number of scans. The rule was right to\n // decline (a :focus rule might have been among the dropped ones); the collector was wrong to\n // make that unavoidable.\n //\n // So the rules a rendered criterion actually READS — :focus styling, orientation locks,\n // pinned positioning, animation — are kept whatever the pool is doing, and truncated now\n // means \"some ordinary rule was dropped\", which is a much weaker statement. A rule can then\n // ask for what it needs instead of refusing on a cap that never concerned it.\n const cssRules = [];\n let unreadable = 0;\n let dropped = 0;\n const MAXR = 4000;\n const camel = (p) => p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());\n const KEEP_SELECTOR = /:focus|:target|:active/i;\n const KEEP_PROP = /^(?:outline|position|animation|transition|transform)/;\n const walk = (list, media) => {\n for (const r of list) {\n if (typeof r.selectorText === 'string' && r.style) {\n const decls = {};\n for (let i = 0; i < r.style.length; i++) {\n const prop = r.style[i];\n decls[camel(prop)] = r.style.getPropertyValue(prop);\n }\n const keep = KEEP_SELECTOR.test(r.selectorText) || Object.keys(decls).some((k) => KEEP_PROP.test(k));\n if (cssRules.length >= MAXR && !keep) { dropped++; continue; }\n cssRules.push(media ? { selector: r.selectorText, media: media, decls: decls } : { selector: r.selectorText, decls: decls });\n } else if (r.cssRules) {\n const cond = r.conditionText || (r.media && r.media.mediaText) || media;\n walk(r.cssRules, cond);\n }\n }\n };\n for (const sheet of document.styleSheets) {\n try {\n walk(sheet.cssRules, undefined);\n } catch (e) {\n unreadable++;\n }\n }\n\n const truncated = els.length > MAX;\n return {\n dom: document.documentElement.outerHTML,\n css: { v: 1, rules: cssRules, unreadable: unreadable, truncated: dropped > 0, dropped: dropped },\n title: document.title,\n // The doctype is NOT part of documentElement.outerHTML, so a capture that records only\n // the DOM drops it — and RGAA 8.1 (is a doctype present, valid, and before <html>?) then\n // has nothing to look at. Recorded as its own field for that reason.\n doctype: document.doctype ? '<!DOCTYPE ' + document.doctype.name + (document.doctype.publicId ? ' PUBLIC \"' + document.doctype.publicId + '\"' : '') + (document.doctype.systemId ? ' \"' + document.doctype.systemId + '\"' : '') + '>' : '',\n lang: document.documentElement.getAttribute('lang') || '',\n url: location.href,\n viewport: { width: window.innerWidth, height: window.innerHeight },\n styles: { v: 1, entries: styles, truncated: truncated },\n boxes: { v: 1, entries: boxes, truncated: truncated }\n };\n})()";
7
7
  const browser = await chromium.launch({ args: ["--no-sandbox", "--disable-dev-shm-usage"] });
8
8
  try {
9
9
  const page = await browser.newPage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.4.1",
3
+ "version": "5.5.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",