pour-engine 1.26.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.
package/config/project.config.js
CHANGED
package/engine/lib/contrast.js
CHANGED
|
@@ -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
|
/**
|
|
@@ -938,17 +982,60 @@ export function textShadowNegligible(cssText, fontSize) {
|
|
|
938
982
|
* hit-test can't reach. Open shadow roots aren't walked here; a known
|
|
939
983
|
* approximation that errs toward failing (never toward a false pass).
|
|
940
984
|
*/
|
|
985
|
+
/**
|
|
986
|
+
* The part of an element's border box its ancestors allow it to PAINT.
|
|
987
|
+
* An object-fit: cover video is routinely taller than its overflow-hidden
|
|
988
|
+
* crop, and the spilled region is guaranteed blank — treating the raw box
|
|
989
|
+
* as a possible backdrop sent captions sitting 16px past a cropped video
|
|
990
|
+
* to review. The walk respects positioning: an absolutely positioned
|
|
991
|
+
* element escapes the clips of ancestors below its containing block
|
|
992
|
+
* (position or transform/filter establish one), and a fixed element's
|
|
993
|
+
* clippers are rare enough that the walk stops rather than guess — every
|
|
994
|
+
* uncertain path keeps the FULL box, erring toward review, never past a
|
|
995
|
+
* real overlap. Returns null when the clip leaves nothing paintable.
|
|
996
|
+
*/
|
|
997
|
+
export function paintableRect(element, rect) {
|
|
998
|
+
let { left, top, right, bottom } = rect;
|
|
999
|
+
let mode = getComputedStyle(element).position;
|
|
1000
|
+
for (let a = element.parentElement; a; a = a.parentElement) {
|
|
1001
|
+
if (mode === 'fixed') break; // conservative: no clip assumed above
|
|
1002
|
+
const s = getComputedStyle(a);
|
|
1003
|
+
const containingBlock = s.position !== 'static' || s.transform !== 'none' || (s.filter && s.filter !== 'none');
|
|
1004
|
+
if (mode === 'absolute' && !containingBlock) continue; // escapes this ancestor's clip
|
|
1005
|
+
// Only overflow: hidden/clip cut paint scroll-independently. A
|
|
1006
|
+
// scrollable ancestor (auto/scroll) clips the CURRENT scroll state —
|
|
1007
|
+
// a carousel slide parked outside the strip swings back under the
|
|
1008
|
+
// user's finger, image and its overlaid caption together, so its
|
|
1009
|
+
// media must keep counting as a possible backdrop (BBC's promo
|
|
1010
|
+
// carousel sent two white-on-photo headlines to a false pass here).
|
|
1011
|
+
const clips = (o) => o === 'hidden' || o === 'clip';
|
|
1012
|
+
if (clips(s.overflowX) || clips(s.overflowY)) {
|
|
1013
|
+
const b = a.getBoundingClientRect();
|
|
1014
|
+
if (clips(s.overflowX)) { left = Math.max(left, b.left); right = Math.min(right, b.right); }
|
|
1015
|
+
if (clips(s.overflowY)) { top = Math.max(top, b.top); bottom = Math.min(bottom, b.bottom); }
|
|
1016
|
+
if (right <= left || bottom <= top) return null;
|
|
1017
|
+
}
|
|
1018
|
+
// Above a consumed ancestor the element is clipped exactly as that
|
|
1019
|
+
// ancestor is: continue the walk in its shoes.
|
|
1020
|
+
mode = s.position;
|
|
1021
|
+
}
|
|
1022
|
+
return { left, top, right, bottom, width: right - left, height: bottom - top,
|
|
1023
|
+
x: left, y: top };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
941
1026
|
export function mediaRects(doc) {
|
|
942
1027
|
if (!mediaRectsCache) {
|
|
943
1028
|
mediaRectsCache = [];
|
|
944
1029
|
for (const el of doc.querySelectorAll('img, video, canvas, svg')) {
|
|
945
|
-
|
|
1030
|
+
// The box an ancestor clips away can never paint, so it is not a
|
|
1031
|
+
// backdrop hazard however far it reaches.
|
|
1032
|
+
const rect = paintableRect(el, el.getBoundingClientRect());
|
|
946
1033
|
// Sub-icon-sized media can't plausibly serve as a text backdrop.
|
|
947
1034
|
// Decorative media with pointer-events: none (animated gradient
|
|
948
1035
|
// canvases, ambient video) paints under text yet never appears in
|
|
949
1036
|
// elementsFromPoint stacks — flagged so callers can distrust even a
|
|
950
1037
|
// hit-test-verified backdrop where such media overlaps.
|
|
951
|
-
if (rect.width >= 8 && rect.height >= 8) {
|
|
1038
|
+
if (rect && rect.width >= 8 && rect.height >= 8) {
|
|
952
1039
|
mediaRectsCache.push({ element: el, rect, hitTestBlind: getComputedStyle(el).pointerEvents === 'none' });
|
|
953
1040
|
}
|
|
954
1041
|
}
|
|
@@ -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:
|
|
142
|
-
//
|
|
143
|
-
|
|
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
|
|
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