pour-engine 1.25.0 → 1.27.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
|
@@ -938,17 +938,60 @@ export function textShadowNegligible(cssText, fontSize) {
|
|
|
938
938
|
* hit-test can't reach. Open shadow roots aren't walked here; a known
|
|
939
939
|
* approximation that errs toward failing (never toward a false pass).
|
|
940
940
|
*/
|
|
941
|
+
/**
|
|
942
|
+
* The part of an element's border box its ancestors allow it to PAINT.
|
|
943
|
+
* An object-fit: cover video is routinely taller than its overflow-hidden
|
|
944
|
+
* crop, and the spilled region is guaranteed blank — treating the raw box
|
|
945
|
+
* as a possible backdrop sent captions sitting 16px past a cropped video
|
|
946
|
+
* to review. The walk respects positioning: an absolutely positioned
|
|
947
|
+
* element escapes the clips of ancestors below its containing block
|
|
948
|
+
* (position or transform/filter establish one), and a fixed element's
|
|
949
|
+
* clippers are rare enough that the walk stops rather than guess — every
|
|
950
|
+
* uncertain path keeps the FULL box, erring toward review, never past a
|
|
951
|
+
* real overlap. Returns null when the clip leaves nothing paintable.
|
|
952
|
+
*/
|
|
953
|
+
export function paintableRect(element, rect) {
|
|
954
|
+
let { left, top, right, bottom } = rect;
|
|
955
|
+
let mode = getComputedStyle(element).position;
|
|
956
|
+
for (let a = element.parentElement; a; a = a.parentElement) {
|
|
957
|
+
if (mode === 'fixed') break; // conservative: no clip assumed above
|
|
958
|
+
const s = getComputedStyle(a);
|
|
959
|
+
const containingBlock = s.position !== 'static' || s.transform !== 'none' || (s.filter && s.filter !== 'none');
|
|
960
|
+
if (mode === 'absolute' && !containingBlock) continue; // escapes this ancestor's clip
|
|
961
|
+
// Only overflow: hidden/clip cut paint scroll-independently. A
|
|
962
|
+
// scrollable ancestor (auto/scroll) clips the CURRENT scroll state —
|
|
963
|
+
// a carousel slide parked outside the strip swings back under the
|
|
964
|
+
// user's finger, image and its overlaid caption together, so its
|
|
965
|
+
// media must keep counting as a possible backdrop (BBC's promo
|
|
966
|
+
// carousel sent two white-on-photo headlines to a false pass here).
|
|
967
|
+
const clips = (o) => o === 'hidden' || o === 'clip';
|
|
968
|
+
if (clips(s.overflowX) || clips(s.overflowY)) {
|
|
969
|
+
const b = a.getBoundingClientRect();
|
|
970
|
+
if (clips(s.overflowX)) { left = Math.max(left, b.left); right = Math.min(right, b.right); }
|
|
971
|
+
if (clips(s.overflowY)) { top = Math.max(top, b.top); bottom = Math.min(bottom, b.bottom); }
|
|
972
|
+
if (right <= left || bottom <= top) return null;
|
|
973
|
+
}
|
|
974
|
+
// Above a consumed ancestor the element is clipped exactly as that
|
|
975
|
+
// ancestor is: continue the walk in its shoes.
|
|
976
|
+
mode = s.position;
|
|
977
|
+
}
|
|
978
|
+
return { left, top, right, bottom, width: right - left, height: bottom - top,
|
|
979
|
+
x: left, y: top };
|
|
980
|
+
}
|
|
981
|
+
|
|
941
982
|
export function mediaRects(doc) {
|
|
942
983
|
if (!mediaRectsCache) {
|
|
943
984
|
mediaRectsCache = [];
|
|
944
985
|
for (const el of doc.querySelectorAll('img, video, canvas, svg')) {
|
|
945
|
-
|
|
986
|
+
// The box an ancestor clips away can never paint, so it is not a
|
|
987
|
+
// backdrop hazard however far it reaches.
|
|
988
|
+
const rect = paintableRect(el, el.getBoundingClientRect());
|
|
946
989
|
// Sub-icon-sized media can't plausibly serve as a text backdrop.
|
|
947
990
|
// Decorative media with pointer-events: none (animated gradient
|
|
948
991
|
// canvases, ambient video) paints under text yet never appears in
|
|
949
992
|
// elementsFromPoint stacks — flagged so callers can distrust even a
|
|
950
993
|
// hit-test-verified backdrop where such media overlaps.
|
|
951
|
-
if (rect.width >= 8 && rect.height >= 8) {
|
|
994
|
+
if (rect && rect.width >= 8 && rect.height >= 8) {
|
|
952
995
|
mediaRectsCache.push({ element: el, rect, hitTestBlind: getComputedStyle(el).pointerEvents === 'none' });
|
|
953
996
|
}
|
|
954
997
|
}
|
|
@@ -1296,5 +1339,6 @@ export function rangeVerdict(foreground, range, required) {
|
|
|
1296
1339
|
export function isLargeText(style) {
|
|
1297
1340
|
const size = parseFloat(style.fontSize);
|
|
1298
1341
|
const weight = parseInt(style.fontWeight, 10) || 400;
|
|
1299
|
-
|
|
1342
|
+
// 18pt and 14pt at CSS's 96dpi: 24px, and 56/3 = 18.666…px exactly.
|
|
1343
|
+
return size >= 24 || (size >= 56 / 3 && weight >= 700);
|
|
1300
1344
|
}
|
|
@@ -876,9 +876,25 @@ export function createContrastRule({ id, tags, help, helpUrl, thresholds }) {
|
|
|
876
876
|
: ''))
|
|
877
877
|
: '';
|
|
878
878
|
|
|
879
|
+
// When the colour pair already clears the LARGE-scale minimum, the miss
|
|
880
|
+
// is about scale, not colour: name the boundary (14pt bold / 18pt), or
|
|
881
|
+
// a bold-but-just-under button reads as the checker ignoring its weight.
|
|
882
|
+
const largeScaleNote = (() => {
|
|
883
|
+
if (required !== thresholds.normal || ratio < thresholds.large) return '';
|
|
884
|
+
const sizePx = parseFloat(style.fontSize) || 0;
|
|
885
|
+
const weightNum = parseInt(style.fontWeight, 10) || 400;
|
|
886
|
+
const shown = `${Math.round(sizePx * 10) / 10}px`;
|
|
887
|
+
if (weightNum >= 700 && sizePx < 56 / 3) {
|
|
888
|
+
return ` This text is bold at ${shown}; bold text counts as large scale from 18.67px (14pt), where the ${thresholds.large}:1 minimum would apply and these colours would pass.`;
|
|
889
|
+
}
|
|
890
|
+
if (weightNum < 700 && sizePx >= 56 / 3 && sizePx < 24) {
|
|
891
|
+
return ` At ${shown} regular weight this is not large scale; from 24px (18pt), or bold at this size, the ${thresholds.large}:1 minimum would apply and these colours would pass.`;
|
|
892
|
+
}
|
|
893
|
+
return '';
|
|
894
|
+
})();
|
|
879
895
|
return {
|
|
880
896
|
status: 'fail',
|
|
881
|
-
message: `Contrast is ${showRatio(ratio)}:1 — below the ${required}:1 WCAG minimum for this text size
|
|
897
|
+
message: `Contrast is ${showRatio(ratio)}:1 — below the ${required}:1 WCAG minimum for this text size.${largeScaleNote}`,
|
|
882
898
|
fix: `Darken the text or lighten the background until the ratio reaches ${required}:1 (currently ${foregroundRgb} on ${backgroundRgb}).`
|
|
883
899
|
+ (authored ? ` Your CSS writes ${authored}, so searching it for the sRGB values above won't find them.` : '')
|
|
884
900
|
+ dimmedNote,
|
package/package.json
CHANGED