pour-engine 1.27.0 → 1.28.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.
@@ -6,6 +6,6 @@
6
6
  export default {
7
7
  engine: {
8
8
  name: 'pour engine',
9
- version: '1.27.0',
9
+ version: '1.28.0',
10
10
  },
11
11
  };
@@ -130,6 +130,7 @@ let opacityAnimatorsCache = null;
130
130
  let mediaRectsCache = null;
131
131
  let panelRectsCache = null;
132
132
  let pseudoCache = new WeakMap();
133
+ let zeroClipCache = new WeakMap();
133
134
  const HAS_IMAGE = Symbol('background-image in chain');
134
135
 
135
136
  export function resetAuditCaches() {
@@ -139,6 +140,49 @@ export function resetAuditCaches() {
139
140
  mediaRectsCache = null;
140
141
  panelRectsCache = null;
141
142
  pseudoCache = new WeakMap();
143
+ zeroClipCache = new WeakMap();
144
+ }
145
+
146
+ /**
147
+ * True when the element sits inside a subtree erased by a ZERO-AREA clip —
148
+ * the sr-only recipe (clip: rect(0,0,0,0) / clip-path: inset(50%) on an
149
+ * absolutely positioned wrapper) applied to an ANCESTOR, which is where it
150
+ * usually lives: a chart's accessible data table hides the table, and the
151
+ * judged element is a cell inside it. Only provably zero-area patterns
152
+ * count: a clip rect with no width or height (any `auto` component keeps
153
+ * the element judged), or a percentage inset consuming a full axis.
154
+ * Partial clips never hide. Verdicts cache per ancestor, so a page pays
155
+ * per container, not per descendant.
156
+ */
157
+ export function inZeroClipSubtree(element) {
158
+ for (let a = element; a; a = a.parentElement) {
159
+ let hidden = zeroClipCache.get(a);
160
+ if (hidden === undefined) {
161
+ const s = getComputedStyle(a);
162
+ hidden = false;
163
+ if (s.clip !== 'auto' && s.position !== 'static') {
164
+ const m = /rect\(([^)]+)\)/.exec(s.clip);
165
+ if (m) {
166
+ const parts = m[1].split(',').map((v) => v.trim());
167
+ if (!parts.includes('auto') && parts.length === 4) {
168
+ const [t, r, b, l] = parts.map(parseFloat);
169
+ if (r - l <= 0 || b - t <= 0) hidden = true;
170
+ }
171
+ }
172
+ }
173
+ if (!hidden && s.clipPath && s.clipPath !== 'none') {
174
+ const m = /inset\(([^)]+)\)/.exec(s.clipPath);
175
+ if (m && !m[1].includes('px')) {
176
+ const parts = m[1].trim().split(/\s+/).map(parseFloat);
177
+ const [t, r = t, b = t, l = r] = parts;
178
+ if (t + b >= 100 || l + r >= 100) hidden = true;
179
+ }
180
+ }
181
+ zeroClipCache.set(a, hidden);
182
+ }
183
+ if (hidden) return true;
184
+ }
185
+ return false;
142
186
  }
143
187
 
144
188
  /**
@@ -3,7 +3,7 @@ import {
3
3
  parseColor, contrastRatio, composite, effectiveBackground, backgroundObscured,
4
4
  backgroundImageSource, backgroundImagePaintRect, imagePaintRectInBox, imageLuminanceRange,
5
5
  gradientLuminanceRange, rangeVerdict, rangeWithBackdrop, isLargeText, cumulativeOpacity,
6
- opacityAnimating, restingOpacity, mediaRects,
6
+ opacityAnimating, restingOpacity, mediaRects, inZeroClipSubtree,
7
7
  paintedBackdrop, opaquePanelRects, viewportVeil, textShadowHalo, textShadowNegligible,
8
8
  pseudoBackdropForText, filmedContrastBounds, backgroundColorSource, scrimPaint, applyOverlays,
9
9
  showRatio,
@@ -138,13 +138,15 @@ function textVisuallyHidden(element, style, foreground) {
138
138
  const indent = parseFloat(style.textIndent) || 0;
139
139
  const clipped = /hidden|clip/.test(`${style.overflow} ${style.overflowX} ${style.overflowY}`)
140
140
  || style.clipPath !== 'none' || (style.clip !== 'auto' && style.position === 'absolute');
141
- // Nothing hints at hiding: skip the getBoundingClientRect layout read —
142
- // this runs for every text element on the page, and most are plainly shown.
143
- if (!clipped && Math.abs(indent) <= 1000) return false;
141
+ // Nothing on the element itself hints at hiding: the recipe may still sit
142
+ // on a WRAPPER (a chart's sr-only data table hides the table; the judged
143
+ // cell inside computes clip: auto) — the ancestor walk is cached per
144
+ // container, so this stays affordable on every text element.
145
+ if (!clipped && Math.abs(indent) <= 1000) return inZeroClipSubtree(element);
144
146
  const rect = element.getBoundingClientRect();
145
147
  if ((rect.width <= 1 || rect.height <= 1) && clipped) return true;
146
148
  if (Math.abs(indent) > rect.width && (clipped || Math.abs(indent) > 1000)) return true;
147
- return false;
149
+ return inZeroClipSubtree(element);
148
150
  }
149
151
 
150
152
  // Turn a luminance-range bracket into a rule outcome (or null = can't tell).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pour-engine",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "description": "Accessibility (WCAG 2.2) audit engine written from the W3C specifications — runs entirely in the browser, no dependencies.",
5
5
  "type": "module",
6
6
  "main": "engine/index.js",