@surea11y/core 1.3.0 → 1.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/CHANGELOG.md +87 -2
- package/README.md +109 -35
- package/bin/surea11y-core.js +20 -0
- package/docs/API_STABILITY.md +26 -0
- package/docs/ARIA_DEPRECATION.md +95 -0
- package/docs/CI_INTEGRATIONS.md +7 -7
- package/docs/ENGINE_OPTIONS.md +1 -1
- package/docs/I18N.md +12 -9
- package/docs/INTEGRATION.md +1 -1
- package/docs/LIMITATIONS.md +1 -1
- package/docs/REPORT.md +1 -1
- package/docs/RULE_CATALOG.md +6 -6
- package/package.json +52 -16
- package/src/baseline.js +0 -0
- package/src/checks/automatic/area-alt-present.js +4 -6
- package/src/checks/automatic/aria-allowed-attr.js +663 -134
- package/src/checks/automatic/aria-allowed-role.js +2 -0
- package/src/checks/automatic/aria-braille-equivalent.js +2 -0
- package/src/checks/automatic/aria-conditional-attr.js +8 -7
- package/src/checks/automatic/aria-deprecated-role.js +107 -38
- package/src/checks/automatic/aria-hidden-body.js +6 -4
- package/src/checks/automatic/aria-hidden-focus.js +12 -13
- package/src/checks/automatic/aria-prohibited-attr.js +98 -105
- package/src/checks/automatic/aria-prohibited-children.js +56 -87
- package/src/checks/automatic/aria-required-attr.js +6 -7
- package/src/checks/automatic/aria-required-children.js +7 -10
- package/src/checks/automatic/aria-required-parent.js +20 -25
- package/src/checks/automatic/aria-role-name-present.js +2 -0
- package/src/checks/automatic/aria-roles-valid.js +33 -6
- package/src/checks/automatic/aria-valid-attr-value.js +18 -15
- package/src/checks/automatic/aria-valid-attr.js +2 -0
- package/src/checks/automatic/autocomplete-valid.js +39 -1
- package/src/checks/automatic/avoid-inline-spacing.js +181 -20
- package/src/checks/automatic/binary-control-name-present.js +13 -3
- package/src/checks/automatic/button-name-present.js +54 -21
- package/src/checks/automatic/canvas-text-alternative-present.js +17 -8
- package/src/checks/automatic/combobox-name-present.js +10 -1
- package/src/checks/automatic/contrast-computable.js +2 -0
- package/src/checks/automatic/contrast-enhanced.js +2 -0
- package/src/checks/automatic/contrast-minimum.js +2 -0
- package/src/checks/automatic/css-orientation-lock.js +21 -27
- package/src/checks/automatic/definition-list-children-valid.js +6 -6
- package/src/checks/automatic/deprecated-elements-not-used.js +4 -2
- package/src/checks/automatic/dialog-name-present.js +18 -11
- package/src/checks/automatic/dlitem-parent-valid.js +2 -0
- package/src/checks/automatic/duplicate-id-aria.js +4 -3
- package/src/checks/automatic/embed-text-alternative-present.js +2 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +50 -4
- package/src/checks/automatic/form-control-single-label.js +110 -43
- package/src/checks/automatic/html-xml-lang-mismatch.js +2 -0
- package/src/checks/automatic/iframe-focusable-content.js +246 -18
- package/src/checks/automatic/iframe-name-present.js +2 -0
- package/src/checks/automatic/iframe-title-unique.js +3 -1
- package/src/checks/automatic/img-alt-present.js +23 -18
- package/src/checks/automatic/input-image-alt-present.js +101 -52
- package/src/checks/automatic/label-in-name.js +97 -27
- package/src/checks/automatic/language-page-present.js +7 -1
- package/src/checks/automatic/link-in-text-block.js +2 -0
- package/src/checks/automatic/link-name-present.js +52 -17
- package/src/checks/automatic/list-children-valid.js +14 -24
- package/src/checks/automatic/listbox-name-present.js +10 -1
- package/src/checks/automatic/listitem-parent-valid.js +30 -7
- package/src/checks/automatic/menuitem-name-present.js +10 -1
- package/src/checks/automatic/meta-refresh-no-exceptions.js +33 -4
- package/src/checks/automatic/meta-refresh-timing-absent.js +32 -4
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +39 -15
- package/src/checks/automatic/meter-name-present.js +12 -4
- package/src/checks/automatic/nested-interactive-controls-absent.js +177 -25
- package/src/checks/automatic/object-text-alternative-present.js +16 -7
- package/src/checks/automatic/option-name-present.js +10 -1
- package/src/checks/automatic/page-title-present.js +2 -0
- package/src/checks/automatic/progressbar-name-present.js +16 -11
- package/src/checks/automatic/role-img-alt-present.js +4 -4
- package/src/checks/automatic/searchbox-name-present.js +10 -1
- package/src/checks/automatic/server-side-image-map-absent.js +4 -3
- package/src/checks/automatic/slider-name-present.js +13 -2
- package/src/checks/automatic/spinbutton-name-present.js +10 -1
- package/src/checks/automatic/summary-name-present.js +10 -1
- package/src/checks/automatic/svg-image-text-alternative-present.js +2 -0
- package/src/checks/automatic/svg-text-alternative-present.js +17 -5
- package/src/checks/automatic/tab-name-present.js +10 -1
- package/src/checks/automatic/table-headers-attr-valid.js +3 -2
- package/src/checks/automatic/table-th-has-data-cells.js +69 -6
- package/src/checks/automatic/target-size-minimum.js +5 -0
- package/src/checks/automatic/td-has-header.js +24 -1
- package/src/checks/automatic/textbox-name-present.js +10 -1
- package/src/checks/automatic/tooltip-name-present.js +10 -1
- package/src/checks/automatic/treeitem-name-present.js +10 -1
- package/src/checks/automatic/valid-lang.js +18 -3
- package/src/checks/automatic/video-poster-text-alternative-present.js +2 -0
- package/src/checks/manual/accesskeys-manual.js +3 -1
- package/src/checks/manual/area-alt-decorative-manual.js +2 -0
- package/src/checks/manual/area-alt-quality-manual.js +2 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +14 -23
- package/src/checks/manual/aria-text-manual.js +6 -5
- package/src/checks/manual/bypass-blocks-present-manual.js +279 -0
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/css-hidden-focus.js +184 -9
- package/src/checks/manual/embed-text-alternative-quality-manual.js +18 -13
- package/src/checks/manual/empty-heading-manual.js +17 -17
- package/src/checks/manual/empty-table-header-manual.js +52 -25
- package/src/checks/manual/focus-order-semantics-manual.js +16 -4
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +2 -0
- package/src/checks/manual/heading-order-manual.js +28 -1
- package/src/checks/manual/identical-links-same-purpose-manual.js +2 -0
- package/src/checks/manual/image-redundant-alt-manual.js +21 -1
- package/src/checks/manual/img-alt-decorative-manual.js +2 -0
- package/src/checks/manual/img-alt-quality-manual.js +2 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +26 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +2 -0
- package/src/checks/manual/label-title-only-manual.js +29 -22
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +51 -55
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +44 -31
- package/src/checks/manual/landmark-main-is-top-level-manual.js +41 -24
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +16 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +14 -21
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +10 -13
- package/src/checks/manual/landmark-one-main-manual.js +12 -23
- package/src/checks/manual/landmark-unique-manual.js +37 -52
- package/src/checks/manual/link-name-quality-manual.js +2 -0
- package/src/checks/manual/media-transcript-present-manual.js +2 -0
- package/src/checks/manual/meta-viewport-large-manual.js +3 -1
- package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +2 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/p-as-heading-manual.js +2 -0
- package/src/checks/manual/page-has-heading-one-manual.js +12 -11
- package/src/checks/manual/page-title-patterns-manual.js +2 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +51 -37
- package/src/checks/manual/region-manual.js +27 -36
- package/src/checks/manual/scope-attr-valid-manual.js +3 -1
- package/src/checks/manual/scrollable-region-focusable-manual.js +2 -0
- package/src/checks/manual/skip-link-manual.js +7 -6
- package/src/checks/manual/svg-text-alternative-quality-manual.js +2 -0
- package/src/checks/manual/tabindex-manual.js +3 -1
- package/src/checks/manual/table-duplicate-name-manual.js +5 -4
- package/src/checks/manual/table-fake-caption-manual.js +24 -3
- package/src/checks/manual/video-caption-manual.js +2 -0
- package/src/checks/manual-review.js +2 -0
- package/src/core.js +11820 -3317
- package/src/index.js +2 -0
- package/src/report.js +51 -9
- package/src/sarif.js +20 -5
- package/surea11y.browser.js +4943 -1388
- package/bin/core.js +0 -473
- package/docs/CLI.md +0 -128
- package/src/catalogs/composites.wcag.js +0 -454
- package/src/checks/automatic/bypass-blocks-present.js +0 -215
- package/src/checks/rules-and-tags.full.csv +0 -19
- package/src/checks/rules-and-tags.full.json +0 -259
- package/src/core/aria-helpers.js +0 -1211
- package/src/core/contrast-helpers.js +0 -1302
- package/src/core/dom-helpers.js +0 -4493
- package/src/core/dom-runner.js +0 -787
- package/src/core/frame-messaging.js +0 -261
- package/src/core/frame-scan.js +0 -190
- package/src/core/rollup-composites.js +0 -127
- package/src/core/rule-meta.js +0 -176
- package/src/coverage/wcag-facets.js +0 -1079
- package/src/coverage/wcag-version-map.js +0 -84
- package/src/i18n/en.js +0 -1228
- package/src/i18n/fr.js +0 -1185
- package/src/policy/contracts.js +0 -18
- package/src/policy/resolvePolicy.js +0 -59
- package/src/policy/schemas/engine-options.schema.json +0 -103
- package/src/policy/schemas/policy-contract.schema.json +0 -40
|
@@ -1,1302 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
function createContrastHelpers(opts, shared) {
|
|
4
|
-
const window = opts && opts.window ? opts.window : null;
|
|
5
|
-
|
|
6
|
-
const trim = shared.trim;
|
|
7
|
-
const computedStyle = shared.computedStyle;
|
|
8
|
-
const composedParent = shared.composedParent;
|
|
9
|
-
const buildSimpleSelector = shared.buildSimpleSelector;
|
|
10
|
-
|
|
11
|
-
const clamp01 = (n) => {
|
|
12
|
-
const x = Number(n);
|
|
13
|
-
if (Number.isNaN(x)) return 0;
|
|
14
|
-
if (x < 0) return 0;
|
|
15
|
-
if (x > 1) return 1;
|
|
16
|
-
return x;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const clamp255 = (n) => {
|
|
20
|
-
const x = Number(n);
|
|
21
|
-
if (Number.isNaN(x)) return 0;
|
|
22
|
-
if (x < 0) return 0;
|
|
23
|
-
if (x > 255) return 255;
|
|
24
|
-
return x;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// -------- Shared per-run caches (shared.__contrastSharedCache lifetime is per engine run) --------
|
|
28
|
-
|
|
29
|
-
function __getSharedWeakMapCache(propName) {
|
|
30
|
-
try {
|
|
31
|
-
const sc = shared && shared.__contrastSharedCache ? shared.__contrastSharedCache : null;
|
|
32
|
-
if (!sc) return null;
|
|
33
|
-
const existing = sc[propName];
|
|
34
|
-
if (existing && typeof existing.get === 'function' && typeof existing.set === 'function')
|
|
35
|
-
return existing;
|
|
36
|
-
const wm = new WeakMap();
|
|
37
|
-
sc[propName] = wm;
|
|
38
|
-
return wm;
|
|
39
|
-
} catch (_e) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function __getSharedTextScanCache() {
|
|
45
|
-
try {
|
|
46
|
-
const sc = shared && shared.__contrastSharedCache ? shared.__contrastSharedCache : null;
|
|
47
|
-
if (!sc) return null;
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
sc.__textScanCache &&
|
|
51
|
-
typeof sc.__textScanCache.get === 'function' &&
|
|
52
|
-
typeof sc.__textScanCache.set === 'function' &&
|
|
53
|
-
typeof sc.__textScanCache.has === 'function'
|
|
54
|
-
) {
|
|
55
|
-
return sc.__textScanCache;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
Object.defineProperty(sc, '__textScanCache', {
|
|
60
|
-
value: new Map(),
|
|
61
|
-
writable: false,
|
|
62
|
-
enumerable: false,
|
|
63
|
-
configurable: true
|
|
64
|
-
});
|
|
65
|
-
} catch {
|
|
66
|
-
sc.__textScanCache = new Map();
|
|
67
|
-
}
|
|
68
|
-
return sc.__textScanCache;
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function __getSharedColorParseCache() {
|
|
75
|
-
try {
|
|
76
|
-
const sc = shared && shared.__contrastSharedCache ? shared.__contrastSharedCache : null;
|
|
77
|
-
if (!sc) return null;
|
|
78
|
-
|
|
79
|
-
if (sc.__colorParseCache && typeof sc.__colorParseCache.get === 'function') {
|
|
80
|
-
return sc.__colorParseCache;
|
|
81
|
-
}
|
|
82
|
-
const m = new Map();
|
|
83
|
-
sc.__colorParseCache = m;
|
|
84
|
-
return m;
|
|
85
|
-
} catch (_e) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// -------- Computed style memoization (per element, per run) --------
|
|
91
|
-
|
|
92
|
-
const __localComputedStyleCache = new WeakMap();
|
|
93
|
-
const __computedStyleCache =
|
|
94
|
-
__getSharedWeakMapCache('__computedStyleCache') || __localComputedStyleCache;
|
|
95
|
-
|
|
96
|
-
function __contrastComputedStyle(el) {
|
|
97
|
-
try {
|
|
98
|
-
if (!el || el.nodeType !== 1) return computedStyle(el);
|
|
99
|
-
if (__computedStyleCache.has(el)) return __computedStyleCache.get(el);
|
|
100
|
-
const cs = computedStyle(el);
|
|
101
|
-
__computedStyleCache.set(el, cs);
|
|
102
|
-
return cs;
|
|
103
|
-
} catch {
|
|
104
|
-
// Always no-throw: return empty object on any failure
|
|
105
|
-
try {
|
|
106
|
-
const cs = computedStyle(el);
|
|
107
|
-
if (el && el.nodeType === 1) __computedStyleCache.set(el, cs);
|
|
108
|
-
return cs;
|
|
109
|
-
} catch {
|
|
110
|
-
return {};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Cache common booleans per element (per run)
|
|
116
|
-
const __localHasBgImgCache = new WeakMap();
|
|
117
|
-
const __localHasBlendModeCache = new WeakMap();
|
|
118
|
-
const __localHasFilterCache = new WeakMap();
|
|
119
|
-
const __hasBgImgCache = __getSharedWeakMapCache('__hasBgImgCache') || __localHasBgImgCache;
|
|
120
|
-
const __hasBlendModeCache =
|
|
121
|
-
__getSharedWeakMapCache('__hasBlendModeCache') || __localHasBlendModeCache;
|
|
122
|
-
const __hasFilterCache = __getSharedWeakMapCache('__hasFilterCache') || __localHasFilterCache;
|
|
123
|
-
|
|
124
|
-
// -------- Visibility mode resolution for getTextScan --------
|
|
125
|
-
|
|
126
|
-
function __getVisibilityMode(engineOptions) {
|
|
127
|
-
const m =
|
|
128
|
-
engineOptions && typeof engineOptions.visibilityMode === 'string'
|
|
129
|
-
? engineOptions.visibilityMode
|
|
130
|
-
: '';
|
|
131
|
-
return m || 'styleOnly';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function __resolveVisibilityMode(ctx, engineOptions, d, w) {
|
|
135
|
-
// If getTextScan was called with a direct string (unlikely, but safe)
|
|
136
|
-
if (typeof engineOptions === 'string') return engineOptions;
|
|
137
|
-
|
|
138
|
-
const candidates = [
|
|
139
|
-
engineOptions,
|
|
140
|
-
|
|
141
|
-
// common shapes
|
|
142
|
-
ctx && ctx.engineOptions,
|
|
143
|
-
ctx && ctx.options && ctx.options.engineOptions,
|
|
144
|
-
ctx && ctx.options,
|
|
145
|
-
ctx && ctx.opts && ctx.opts.engineOptions,
|
|
146
|
-
ctx && ctx.opts,
|
|
147
|
-
|
|
148
|
-
// policy layering shapes
|
|
149
|
-
ctx && ctx.policyOverrides && ctx.policyOverrides.engineOptions,
|
|
150
|
-
ctx && ctx.policyOverrides,
|
|
151
|
-
ctx && ctx.policy && ctx.policy.engineOptions,
|
|
152
|
-
ctx && ctx.policy,
|
|
153
|
-
|
|
154
|
-
// sometimes hoisted
|
|
155
|
-
ctx,
|
|
156
|
-
|
|
157
|
-
// opts passed into createContrastHelpers
|
|
158
|
-
opts && opts.engineOptions,
|
|
159
|
-
opts && opts.options && opts.options.engineOptions,
|
|
160
|
-
opts && opts.options,
|
|
161
|
-
opts && opts.opts && opts.opts.engineOptions,
|
|
162
|
-
opts && opts.opts,
|
|
163
|
-
|
|
164
|
-
opts && opts.policyOverrides && opts.policyOverrides.engineOptions,
|
|
165
|
-
opts && opts.policyOverrides,
|
|
166
|
-
opts && opts.policy && opts.policy.engineOptions,
|
|
167
|
-
opts && opts.policy,
|
|
168
|
-
|
|
169
|
-
// globals sometimes used by runners
|
|
170
|
-
w && w.__a11ycoreEngineOptions,
|
|
171
|
-
d && d.__a11ycoreEngineOptions
|
|
172
|
-
];
|
|
173
|
-
|
|
174
|
-
for (const c of candidates) {
|
|
175
|
-
if (!c || typeof c !== 'object') continue;
|
|
176
|
-
|
|
177
|
-
if (typeof c.visibilityMode === 'string') return c.visibilityMode;
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
c.engineOptions &&
|
|
181
|
-
typeof c.engineOptions === 'object' &&
|
|
182
|
-
typeof c.engineOptions.visibilityMode === 'string'
|
|
183
|
-
) {
|
|
184
|
-
return c.engineOptions.visibilityMode;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
c.options &&
|
|
189
|
-
typeof c.options === 'object' &&
|
|
190
|
-
c.options.engineOptions &&
|
|
191
|
-
typeof c.options.engineOptions === 'object' &&
|
|
192
|
-
typeof c.options.engineOptions.visibilityMode === 'string'
|
|
193
|
-
) {
|
|
194
|
-
return c.options.engineOptions.visibilityMode;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
c.policy &&
|
|
199
|
-
typeof c.policy === 'object' &&
|
|
200
|
-
c.policy.engineOptions &&
|
|
201
|
-
typeof c.policy.engineOptions === 'object' &&
|
|
202
|
-
typeof c.policy.engineOptions.visibilityMode === 'string'
|
|
203
|
-
) {
|
|
204
|
-
return c.policy.engineOptions.visibilityMode;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
c.policyOverrides &&
|
|
209
|
-
typeof c.policyOverrides === 'object' &&
|
|
210
|
-
c.policyOverrides.engineOptions &&
|
|
211
|
-
typeof c.policyOverrides.engineOptions === 'object' &&
|
|
212
|
-
typeof c.policyOverrides.engineOptions.visibilityMode === 'string'
|
|
213
|
-
) {
|
|
214
|
-
return c.policyOverrides.engineOptions.visibilityMode;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return 'styleOnly';
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function __asEligibilityBool(v) {
|
|
222
|
-
if (typeof v === 'boolean') return v;
|
|
223
|
-
if (v && typeof v === 'object' && typeof v.eligible === 'boolean') return v.eligible;
|
|
224
|
-
return !!v;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// WCAG 1.4.3/1.4.6 "Incidental" exception: text that is part of an inactive
|
|
228
|
-
// (disabled) user interface component has no contrast requirement. Walk the
|
|
229
|
-
// ancestor chain (not just the text's immediate parent) so text nested inside
|
|
230
|
-
// a disabled control, e.g. <button disabled><span>Label</span></button>, is
|
|
231
|
-
// still recognized as inactive.
|
|
232
|
-
function isInactiveUiComponent(el) {
|
|
233
|
-
let node = el;
|
|
234
|
-
let depth = 0;
|
|
235
|
-
while (node && node.nodeType === 1 && depth++ < 100) {
|
|
236
|
-
try {
|
|
237
|
-
if (typeof node.matches === 'function' && node.matches(':disabled')) return true;
|
|
238
|
-
} catch {}
|
|
239
|
-
try {
|
|
240
|
-
const ad = node.getAttribute
|
|
241
|
-
? String(node.getAttribute('aria-disabled') || '')
|
|
242
|
-
.trim()
|
|
243
|
-
.toLowerCase()
|
|
244
|
-
: '';
|
|
245
|
-
if (ad === 'true') return true;
|
|
246
|
-
} catch {}
|
|
247
|
-
node = node.parentElement;
|
|
248
|
-
}
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function getTextScan(ctx, helpers, engineOptions) {
|
|
253
|
-
try {
|
|
254
|
-
const d = (ctx && ctx.document) || (opts && opts.document) || null;
|
|
255
|
-
|
|
256
|
-
const w = (ctx && ctx.window) || (d && d.defaultView) || window || null;
|
|
257
|
-
|
|
258
|
-
const rawMode = __resolveVisibilityMode(ctx, engineOptions, d, w);
|
|
259
|
-
|
|
260
|
-
const visibilityMode =
|
|
261
|
-
__getVisibilityMode({ visibilityMode: rawMode }) === 'styleAndGeometry'
|
|
262
|
-
? 'styleAndGeometry'
|
|
263
|
-
: 'styleOnly';
|
|
264
|
-
|
|
265
|
-
if (!d || typeof d.createTreeWalker !== 'function') {
|
|
266
|
-
return { eligibleTextCount: 0, elements: [], visibilityMode };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const cache = __getSharedTextScanCache();
|
|
270
|
-
const cacheKey = `visibilityMode=${visibilityMode}`;
|
|
271
|
-
if (cache && cache.has(cacheKey)) return cache.get(cacheKey);
|
|
272
|
-
|
|
273
|
-
// ctx.root is an array with multi-region contextSelector support
|
|
274
|
-
// (dom-runner.js resolves it that way now) but back-compat with
|
|
275
|
-
// any caller still passing a single element directly.
|
|
276
|
-
const walkRootsRaw =
|
|
277
|
-
ctx && ctx.root
|
|
278
|
-
? Array.isArray(ctx.root)
|
|
279
|
-
? ctx.root
|
|
280
|
-
: [ctx.root]
|
|
281
|
-
: [d.body || d.documentElement || d];
|
|
282
|
-
const walkRoots = walkRootsRaw
|
|
283
|
-
.map((wr) => (wr && wr.nodeType === 9 ? wr.body || wr.documentElement || wr : wr))
|
|
284
|
-
.filter(Boolean);
|
|
285
|
-
|
|
286
|
-
const SHOW_TEXT =
|
|
287
|
-
w && w.NodeFilter && typeof w.NodeFilter.SHOW_TEXT === 'number'
|
|
288
|
-
? w.NodeFilter.SHOW_TEXT
|
|
289
|
-
: 4;
|
|
290
|
-
|
|
291
|
-
const isNonEmptyText = (t) => t != null && /\S/.test(String(t));
|
|
292
|
-
|
|
293
|
-
const elToCount = new WeakMap();
|
|
294
|
-
const elements = [];
|
|
295
|
-
let eligibleTextCount = 0;
|
|
296
|
-
|
|
297
|
-
const eligCache = new WeakMap();
|
|
298
|
-
const inactiveCache = new WeakMap();
|
|
299
|
-
|
|
300
|
-
const isVisibleEligible = (el) => {
|
|
301
|
-
if (!helpers || typeof helpers.isDomVisibleEligible !== 'function') return true;
|
|
302
|
-
if (eligCache.has(el)) return eligCache.get(el);
|
|
303
|
-
|
|
304
|
-
let ok;
|
|
305
|
-
try {
|
|
306
|
-
const r = helpers.isDomVisibleEligible(el, ctx, { visibilityMode });
|
|
307
|
-
ok = __asEligibilityBool(r);
|
|
308
|
-
} catch {
|
|
309
|
-
ok = false;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
eligCache.set(el, ok);
|
|
313
|
-
return ok;
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const isInactive = (el) => {
|
|
317
|
-
if (inactiveCache.has(el)) return inactiveCache.get(el);
|
|
318
|
-
let inactive;
|
|
319
|
-
try {
|
|
320
|
-
inactive = isInactiveUiComponent(el);
|
|
321
|
-
} catch {
|
|
322
|
-
inactive = false;
|
|
323
|
-
}
|
|
324
|
-
inactiveCache.set(el, inactive);
|
|
325
|
-
return inactive;
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
let node = null;
|
|
329
|
-
let guard = 0;
|
|
330
|
-
// Guards against double-counting text nodes reachable from more
|
|
331
|
-
// than one root when contextSelector regions overlap/nest (a
|
|
332
|
-
// single TreeWalker never revisits a node, but running one
|
|
333
|
-
// walker per root could otherwise walk the same subtree twice).
|
|
334
|
-
const visitedTextNodes = new Set();
|
|
335
|
-
|
|
336
|
-
for (const walkRoot of walkRoots) {
|
|
337
|
-
if (guard >= 500000) break;
|
|
338
|
-
let walker;
|
|
339
|
-
try {
|
|
340
|
-
walker = d.createTreeWalker(walkRoot, SHOW_TEXT, null);
|
|
341
|
-
} catch {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
while ((node = walker.nextNode()) && guard++ < 500000) {
|
|
346
|
-
if (visitedTextNodes.has(node)) continue;
|
|
347
|
-
visitedTextNodes.add(node);
|
|
348
|
-
|
|
349
|
-
const text = node && node.nodeValue;
|
|
350
|
-
if (!isNonEmptyText(text)) continue;
|
|
351
|
-
|
|
352
|
-
const el =
|
|
353
|
-
node.parentElement ||
|
|
354
|
-
(node.parentNode && node.parentNode.nodeType === 1 ? node.parentNode : null);
|
|
355
|
-
|
|
356
|
-
if (!el) continue;
|
|
357
|
-
// Respect subtree exclusions from engineOptions.excludeSelectors
|
|
358
|
-
try {
|
|
359
|
-
if (helpers && typeof helpers.isExcluded === 'function' && helpers.isExcluded(el))
|
|
360
|
-
continue;
|
|
361
|
-
} catch {}
|
|
362
|
-
if (!isVisibleEligible(el)) continue;
|
|
363
|
-
if (isInactive(el)) continue;
|
|
364
|
-
|
|
365
|
-
eligibleTextCount++;
|
|
366
|
-
|
|
367
|
-
const prev = elToCount.get(el);
|
|
368
|
-
if (prev === undefined) {
|
|
369
|
-
elToCount.set(el, 1);
|
|
370
|
-
elements.push(el);
|
|
371
|
-
} else {
|
|
372
|
-
elToCount.set(el, prev + 1);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// <input type="submit"|"button"|"reset">'s visible label is
|
|
378
|
-
// rendered from its `value` attribute, not a DOM text node, so
|
|
379
|
-
// it's structurally invisible to the SHOW_TEXT walk above (void
|
|
380
|
-
// elements can't have text-node children at all). Found
|
|
381
|
-
// 2026-08-01: these inputs were silently skipped by both
|
|
382
|
-
// contrast-minimum and contrast-enhanced regardless of contrast
|
|
383
|
-
// mode, confirmed on a real page (progressive.com's
|
|
384
|
-
// `<input type="submit" value="Get a quote">`, a genuine
|
|
385
|
-
// AAA-level failure surea11y never even considered a
|
|
386
|
-
// candidate). Same eligibility gates as the real
|
|
387
|
-
// text-node path above, applied to the input element itself.
|
|
388
|
-
const visitedValueInputs = new Set();
|
|
389
|
-
for (const walkRoot of walkRoots) {
|
|
390
|
-
let candidates;
|
|
391
|
-
try {
|
|
392
|
-
candidates = walkRoot.querySelectorAll(
|
|
393
|
-
'input[type="submit" i], input[type="button" i], input[type="reset" i]'
|
|
394
|
-
);
|
|
395
|
-
} catch {
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
for (const el of candidates) {
|
|
399
|
-
if (visitedValueInputs.has(el)) continue;
|
|
400
|
-
visitedValueInputs.add(el);
|
|
401
|
-
|
|
402
|
-
const value = el.getAttribute ? el.getAttribute('value') : el.value;
|
|
403
|
-
if (!isNonEmptyText(value)) continue;
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
if (helpers && typeof helpers.isExcluded === 'function' && helpers.isExcluded(el))
|
|
407
|
-
continue;
|
|
408
|
-
} catch {}
|
|
409
|
-
if (!isVisibleEligible(el)) continue;
|
|
410
|
-
if (isInactive(el)) continue;
|
|
411
|
-
|
|
412
|
-
eligibleTextCount++;
|
|
413
|
-
|
|
414
|
-
const prev = elToCount.get(el);
|
|
415
|
-
if (prev === undefined) {
|
|
416
|
-
elToCount.set(el, 1);
|
|
417
|
-
elements.push(el);
|
|
418
|
-
} else {
|
|
419
|
-
elToCount.set(el, prev + 1);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const out = Object.freeze({
|
|
425
|
-
eligibleTextCount,
|
|
426
|
-
visibilityMode,
|
|
427
|
-
elements: Object.freeze(
|
|
428
|
-
elements.map((el) => Object.freeze({ el, textCount: elToCount.get(el) || 0 }))
|
|
429
|
-
)
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
if (cache) cache.set(cacheKey, out);
|
|
433
|
-
return out;
|
|
434
|
-
} catch {
|
|
435
|
-
return { eligibleTextCount: 0, elements: [], visibilityMode: 'styleOnly' };
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// -------- Formatting helpers --------
|
|
440
|
-
|
|
441
|
-
function toHex2(n) {
|
|
442
|
-
const x = clamp255(n);
|
|
443
|
-
const s = x.toString(16).toLowerCase();
|
|
444
|
-
return s.length === 1 ? '0' + s : s;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function rgbToHex(rgb) {
|
|
448
|
-
try {
|
|
449
|
-
if (!rgb || typeof rgb !== 'object') return '';
|
|
450
|
-
return '#' + toHex2(rgb.r) + toHex2(rgb.g) + toHex2(rgb.b);
|
|
451
|
-
} catch {
|
|
452
|
-
return '';
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// 96px/in, 72pt/in => 1px = 0.75pt
|
|
457
|
-
function pxToPt(px) {
|
|
458
|
-
const x = parseFloat(px);
|
|
459
|
-
if (!Number.isFinite(x)) return '';
|
|
460
|
-
return (x * 0.75).toFixed(1);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function fontWeightLabel(fontWeightNum) {
|
|
464
|
-
const w = Number(fontWeightNum);
|
|
465
|
-
if (Number.isFinite(w) && w >= 700) return 'bold';
|
|
466
|
-
return 'normal';
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function round2(n) {
|
|
470
|
-
const x = Number(n);
|
|
471
|
-
if (!Number.isFinite(x)) return '0.00';
|
|
472
|
-
return (Math.round(x * 100) / 100).toFixed(2);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function rgbaToString(rgba) {
|
|
476
|
-
if (!rgba || typeof rgba !== 'object') return '';
|
|
477
|
-
const r = clamp255(rgba.r);
|
|
478
|
-
const g = clamp255(rgba.g);
|
|
479
|
-
const b = clamp255(rgba.b);
|
|
480
|
-
const a = clamp01(rgba.a);
|
|
481
|
-
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function parsePx(value) {
|
|
485
|
-
if (value == null) return null;
|
|
486
|
-
|
|
487
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
488
|
-
|
|
489
|
-
const s = String(value).trim().toLowerCase();
|
|
490
|
-
if (!s) return null;
|
|
491
|
-
|
|
492
|
-
const n = parseFloat(s);
|
|
493
|
-
if (!Number.isFinite(n)) return null;
|
|
494
|
-
|
|
495
|
-
if (s.endsWith('px')) return n;
|
|
496
|
-
if (s.endsWith('pt')) return n * (96 / 72);
|
|
497
|
-
|
|
498
|
-
if (s.endsWith('rem')) return n * 16;
|
|
499
|
-
if (s.endsWith('em')) return n * 16;
|
|
500
|
-
if (s.endsWith('%')) return (n / 100) * 16;
|
|
501
|
-
|
|
502
|
-
return n;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function normalizeFontWeight(v) {
|
|
506
|
-
const s = trim(v).toLowerCase();
|
|
507
|
-
if (!s) return 400;
|
|
508
|
-
if (s === 'normal') return 400;
|
|
509
|
-
if (s === 'bold' || s === 'bolder') return 700;
|
|
510
|
-
if (s === 'lighter') return 300;
|
|
511
|
-
const n = Number.parseInt(s, 10);
|
|
512
|
-
return Number.isFinite(n) ? n : 400;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function isLargeText(fontSizePx, fontWeightNum) {
|
|
516
|
-
const size = parseFloat(fontSizePx);
|
|
517
|
-
const w = Number(fontWeightNum);
|
|
518
|
-
if (!Number.isFinite(size)) return false;
|
|
519
|
-
if (size >= 24) return true;
|
|
520
|
-
if (size >= 18.6667 && Number.isFinite(w) && w >= 700) return true;
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function requiredRatio(level, large) {
|
|
525
|
-
const l = String(level || '').toUpperCase();
|
|
526
|
-
if (l === 'AAA') return large ? 4.5 : 7.0;
|
|
527
|
-
return large ? 3.0 : 4.5;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// -------- CSS color parsing + memoization --------
|
|
531
|
-
|
|
532
|
-
const __localColorParseCache = new Map();
|
|
533
|
-
const __colorParseCache = __getSharedColorParseCache() || __localColorParseCache;
|
|
534
|
-
|
|
535
|
-
function __normalizeCssColorCacheKey(input) {
|
|
536
|
-
const raw = input == null ? '' : String(input);
|
|
537
|
-
let s = trim(raw).toLowerCase();
|
|
538
|
-
if (!s) return '';
|
|
539
|
-
s = s.replace(/\s+/g, ' ');
|
|
540
|
-
s = s.replace(/\s*,\s*/g, ',');
|
|
541
|
-
s = s.replace(/\(\s+/g, '(');
|
|
542
|
-
s = s.replace(/\s+\)/g, ')');
|
|
543
|
-
s = s.replace(/\s*\/\s*/g, '/');
|
|
544
|
-
// Only strip whitespace BEFORE a '%' (e.g. "50 %" -> "50%"). Stripping
|
|
545
|
-
// trailing whitespace too (the original /\s*%\s*/ pattern) ate the
|
|
546
|
-
// space that separates adjacent percentage-suffixed channels in the
|
|
547
|
-
// modern space-separated syntax (e.g. "rgb(100% 0% 0%)" collapsed to
|
|
548
|
-
// "rgb(100%0%0%)", one fused token instead of three), silently
|
|
549
|
-
// breaking that whole syntax variant.
|
|
550
|
-
s = s.replace(/\s*%/g, '%');
|
|
551
|
-
return s;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function __parseCssColorToRgbaUncached(input) {
|
|
555
|
-
const s = trim(input).toLowerCase();
|
|
556
|
-
if (!s) return null;
|
|
557
|
-
if (s === 'transparent') return { r: 0, g: 0, b: 0, a: 0 };
|
|
558
|
-
|
|
559
|
-
if (s[0] === '#') {
|
|
560
|
-
const hex = s.slice(1);
|
|
561
|
-
const isHex = /^[0-9a-f]+$/i.test(hex);
|
|
562
|
-
if (!isHex) return null;
|
|
563
|
-
|
|
564
|
-
const hexToInt = (h) => Number.parseInt(h, 16);
|
|
565
|
-
|
|
566
|
-
try {
|
|
567
|
-
if (hex.length === 3) {
|
|
568
|
-
const r = hexToInt(hex[0] + hex[0]);
|
|
569
|
-
const g = hexToInt(hex[1] + hex[1]);
|
|
570
|
-
const b = hexToInt(hex[2] + hex[2]);
|
|
571
|
-
return { r, g, b, a: 1 };
|
|
572
|
-
}
|
|
573
|
-
if (hex.length === 4) {
|
|
574
|
-
const r = hexToInt(hex[0] + hex[0]);
|
|
575
|
-
const g = hexToInt(hex[1] + hex[1]);
|
|
576
|
-
const b = hexToInt(hex[2] + hex[2]);
|
|
577
|
-
const a = hexToInt(hex[3] + hex[3]) / 255;
|
|
578
|
-
return { r, g, b, a: clamp01(a) };
|
|
579
|
-
}
|
|
580
|
-
if (hex.length === 6) {
|
|
581
|
-
const r = hexToInt(hex.slice(0, 2));
|
|
582
|
-
const g = hexToInt(hex.slice(2, 4));
|
|
583
|
-
const b = hexToInt(hex.slice(4, 6));
|
|
584
|
-
return { r, g, b, a: 1 };
|
|
585
|
-
}
|
|
586
|
-
if (hex.length === 8) {
|
|
587
|
-
const r = hexToInt(hex.slice(0, 2));
|
|
588
|
-
const g = hexToInt(hex.slice(2, 4));
|
|
589
|
-
const b = hexToInt(hex.slice(4, 6));
|
|
590
|
-
const a = hexToInt(hex.slice(6, 8)) / 255;
|
|
591
|
-
return { r, g, b, a: clamp01(a) };
|
|
592
|
-
}
|
|
593
|
-
} catch {}
|
|
594
|
-
return null;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const m = s.match(/^rgba?\((.*)\)$/);
|
|
598
|
-
|
|
599
|
-
// Modern space-separated: rgb(0 0 0 / 0.5)
|
|
600
|
-
if (m && m[1] && m[1].indexOf(',') === -1) {
|
|
601
|
-
const body = trim(m[1]);
|
|
602
|
-
const parts2 = body.split('/').map((x) => trim(x));
|
|
603
|
-
const rgbPart = parts2[0] || '';
|
|
604
|
-
const aPart = parts2[1] || '';
|
|
605
|
-
|
|
606
|
-
const rgbNums = rgbPart
|
|
607
|
-
.split(/\s+/)
|
|
608
|
-
.map((x) => trim(x))
|
|
609
|
-
.filter(Boolean);
|
|
610
|
-
if (rgbNums.length >= 3) {
|
|
611
|
-
const parseChannel2 = (t) => {
|
|
612
|
-
if (!t) return null;
|
|
613
|
-
if (t.endsWith('%')) {
|
|
614
|
-
const p = Number.parseFloat(t);
|
|
615
|
-
if (!Number.isFinite(p)) return null;
|
|
616
|
-
return clamp255(Math.round((p / 100) * 255));
|
|
617
|
-
}
|
|
618
|
-
const n = Number.parseFloat(t);
|
|
619
|
-
if (!Number.isFinite(n)) return null;
|
|
620
|
-
return clamp255(Math.round(n));
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
const r = parseChannel2(rgbNums[0]);
|
|
624
|
-
const g = parseChannel2(rgbNums[1]);
|
|
625
|
-
const b = parseChannel2(rgbNums[2]);
|
|
626
|
-
if (r == null || g == null || b == null) return null;
|
|
627
|
-
|
|
628
|
-
let a = 1;
|
|
629
|
-
if (aPart) {
|
|
630
|
-
if (aPart.endsWith('%')) {
|
|
631
|
-
const p = Number.parseFloat(aPart);
|
|
632
|
-
if (Number.isFinite(p)) a = clamp01(p / 100);
|
|
633
|
-
} else {
|
|
634
|
-
const n = Number.parseFloat(aPart);
|
|
635
|
-
if (Number.isFinite(n)) a = clamp01(n);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return { r, g, b, a };
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Comma-separated: rgb(0,0,0) / rgba(0,0,0,0.5)
|
|
644
|
-
if (m && m[1]) {
|
|
645
|
-
const parts = m[1].split(',').map((x) => trim(x));
|
|
646
|
-
if (parts.length < 3) return null;
|
|
647
|
-
|
|
648
|
-
const parseChannel = (t) => {
|
|
649
|
-
if (!t) return null;
|
|
650
|
-
if (t.endsWith('%')) {
|
|
651
|
-
const p = Number.parseFloat(t);
|
|
652
|
-
if (!Number.isFinite(p)) return null;
|
|
653
|
-
return clamp255(Math.round((p / 100) * 255));
|
|
654
|
-
}
|
|
655
|
-
const n = Number.parseFloat(t);
|
|
656
|
-
if (!Number.isFinite(n)) return null;
|
|
657
|
-
return clamp255(Math.round(n));
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
const r = parseChannel(parts[0]);
|
|
661
|
-
const g = parseChannel(parts[1]);
|
|
662
|
-
const b = parseChannel(parts[2]);
|
|
663
|
-
if (r == null || g == null || b == null) return null;
|
|
664
|
-
|
|
665
|
-
let a = 1;
|
|
666
|
-
if (parts.length >= 4) {
|
|
667
|
-
const t = parts[3];
|
|
668
|
-
if (t && t.endsWith('%')) {
|
|
669
|
-
const p = Number.parseFloat(t);
|
|
670
|
-
if (Number.isFinite(p)) a = clamp01(p / 100);
|
|
671
|
-
} else {
|
|
672
|
-
const n = Number.parseFloat(t);
|
|
673
|
-
if (Number.isFinite(n)) a = clamp01(n);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
return { r, g, b, a };
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Fallback: let the platform parse named/system colors.
|
|
680
|
-
// Useful in jsdom/browsers where computed styles may return keywords like "black" or "CanvasText".
|
|
681
|
-
try {
|
|
682
|
-
const w = window || null;
|
|
683
|
-
const d = w && w.document ? w.document : null;
|
|
684
|
-
if (
|
|
685
|
-
w &&
|
|
686
|
-
d &&
|
|
687
|
-
typeof d.createElement === 'function' &&
|
|
688
|
-
typeof w.getComputedStyle === 'function'
|
|
689
|
-
) {
|
|
690
|
-
const probe = d.createElement('span');
|
|
691
|
-
// Avoid layout/paint side effects
|
|
692
|
-
probe.style.position = 'absolute';
|
|
693
|
-
probe.style.left = '-9999px';
|
|
694
|
-
probe.style.top = '-9999px';
|
|
695
|
-
probe.style.opacity = '0';
|
|
696
|
-
probe.style.color = String(input);
|
|
697
|
-
const parent = d.body || d.documentElement;
|
|
698
|
-
if (parent && typeof parent.appendChild === 'function') parent.appendChild(probe);
|
|
699
|
-
|
|
700
|
-
let computed = '';
|
|
701
|
-
try {
|
|
702
|
-
computed = (w.getComputedStyle(probe) && w.getComputedStyle(probe).color) || '';
|
|
703
|
-
} catch (_e) {
|
|
704
|
-
computed = '';
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
try {
|
|
708
|
-
if (probe && probe.parentNode) probe.parentNode.removeChild(probe);
|
|
709
|
-
} catch (_e) {}
|
|
710
|
-
|
|
711
|
-
const normalized = __normalizeCssColorCacheKey(computed);
|
|
712
|
-
if (normalized && normalized !== __normalizeCssColorCacheKey(input)) {
|
|
713
|
-
// Reuse the parser on the computed rgb()/rgba() string.
|
|
714
|
-
const parsed = __parseCssColorToRgbaUncached(normalized);
|
|
715
|
-
if (parsed) return parsed;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
} catch (_e) {}
|
|
719
|
-
|
|
720
|
-
return null;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function parseCssColorToRgba(input) {
|
|
724
|
-
const key = __normalizeCssColorCacheKey(input);
|
|
725
|
-
if (!key) return null;
|
|
726
|
-
if (__colorParseCache.has(key)) return __colorParseCache.get(key);
|
|
727
|
-
|
|
728
|
-
const out = __parseCssColorToRgbaUncached(key);
|
|
729
|
-
__colorParseCache.set(key, out);
|
|
730
|
-
return out;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// -------- Color math --------
|
|
734
|
-
|
|
735
|
-
function compositeRgba(src, dst) {
|
|
736
|
-
const s = src && typeof src === 'object' ? src : { r: 0, g: 0, b: 0, a: 0 };
|
|
737
|
-
const d = dst && typeof dst === 'object' ? dst : { r: 0, g: 0, b: 0, a: 0 };
|
|
738
|
-
|
|
739
|
-
const as = clamp01(s.a);
|
|
740
|
-
const ad = clamp01(d.a);
|
|
741
|
-
|
|
742
|
-
const outA = as + ad * (1 - as);
|
|
743
|
-
if (outA <= 0) return { r: 0, g: 0, b: 0, a: 0 };
|
|
744
|
-
|
|
745
|
-
const rs = clamp255(s.r);
|
|
746
|
-
const gs = clamp255(s.g);
|
|
747
|
-
const bs = clamp255(s.b);
|
|
748
|
-
|
|
749
|
-
const rd = clamp255(d.r);
|
|
750
|
-
const gd = clamp255(d.g);
|
|
751
|
-
const bd = clamp255(d.b);
|
|
752
|
-
|
|
753
|
-
const outR = (rs * as + rd * ad * (1 - as)) / outA;
|
|
754
|
-
const outG = (gs * as + gd * ad * (1 - as)) / outA;
|
|
755
|
-
const outB = (bs * as + bd * ad * (1 - as)) / outA;
|
|
756
|
-
|
|
757
|
-
return {
|
|
758
|
-
r: clamp255(Math.round(outR)),
|
|
759
|
-
g: clamp255(Math.round(outG)),
|
|
760
|
-
b: clamp255(Math.round(outB)),
|
|
761
|
-
a: clamp01(outA)
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function srgbToLinear(c) {
|
|
766
|
-
const cs = Number(c) / 255;
|
|
767
|
-
if (cs <= 0.03928) return cs / 12.92;
|
|
768
|
-
return Math.pow((cs + 0.055) / 1.055, 2.4);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function relativeLuminance(rgb) {
|
|
772
|
-
const r = srgbToLinear(clamp255(rgb.r));
|
|
773
|
-
const g = srgbToLinear(clamp255(rgb.g));
|
|
774
|
-
const b = srgbToLinear(clamp255(rgb.b));
|
|
775
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function contrastRatio(fgRgb, bgRgb) {
|
|
779
|
-
const L1 = relativeLuminance(fgRgb);
|
|
780
|
-
const L2 = relativeLuminance(bgRgb);
|
|
781
|
-
const lighter = Math.max(L1, L2);
|
|
782
|
-
const darker = Math.min(L1, L2);
|
|
783
|
-
return (lighter + 0.05) / (darker + 0.05);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function truncateCssValue(v, maxLen) {
|
|
787
|
-
const s = trim(v);
|
|
788
|
-
const n = (Number(maxLen) | 0) > 10 ? Number(maxLen) | 0 : 80;
|
|
789
|
-
if (s.length <= n) return s;
|
|
790
|
-
return s.slice(0, n - 3) + '...';
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function hasBackgroundImageOrGradient(style) {
|
|
794
|
-
try {
|
|
795
|
-
const v = style && style.backgroundImage;
|
|
796
|
-
return !!(v && String(v).trim() && String(v).trim().toLowerCase() !== 'none');
|
|
797
|
-
} catch {
|
|
798
|
-
return false;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function hasBlendMode(style) {
|
|
803
|
-
try {
|
|
804
|
-
const v = style && style.mixBlendMode;
|
|
805
|
-
return !!(v && String(v).trim() && String(v).trim().toLowerCase() !== 'normal');
|
|
806
|
-
} catch {
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function hasFilter(style) {
|
|
812
|
-
try {
|
|
813
|
-
const f = style && style.filter;
|
|
814
|
-
const bf = style && style.backdropFilter;
|
|
815
|
-
const fOn = f && String(f).trim().toLowerCase() !== 'none';
|
|
816
|
-
const bfOn = bf && String(bf).trim().toLowerCase() !== 'none';
|
|
817
|
-
return !!(fOn || bfOn);
|
|
818
|
-
} catch {
|
|
819
|
-
return false;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function __hasBackgroundImageOrGradientEl(el, cs) {
|
|
824
|
-
try {
|
|
825
|
-
if (!el || el.nodeType !== 1) return hasBackgroundImageOrGradient(cs);
|
|
826
|
-
if (__hasBgImgCache.has(el)) return __hasBgImgCache.get(el);
|
|
827
|
-
const v = hasBackgroundImageOrGradient(cs);
|
|
828
|
-
__hasBgImgCache.set(el, v);
|
|
829
|
-
return v;
|
|
830
|
-
} catch {
|
|
831
|
-
return false;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function __hasBlendModeEl(el, cs) {
|
|
836
|
-
try {
|
|
837
|
-
if (!el || el.nodeType !== 1) return hasBlendMode(cs);
|
|
838
|
-
if (__hasBlendModeCache.has(el)) return __hasBlendModeCache.get(el);
|
|
839
|
-
const v = hasBlendMode(cs);
|
|
840
|
-
__hasBlendModeCache.set(el, v);
|
|
841
|
-
return v;
|
|
842
|
-
} catch {
|
|
843
|
-
return false;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function __hasFilterEl(el, cs) {
|
|
848
|
-
try {
|
|
849
|
-
if (!el || el.nodeType !== 1) return hasFilter(cs);
|
|
850
|
-
if (__hasFilterCache.has(el)) return __hasFilterCache.get(el);
|
|
851
|
-
const v = hasFilter(cs);
|
|
852
|
-
__hasFilterCache.set(el, v);
|
|
853
|
-
return v;
|
|
854
|
-
} catch {
|
|
855
|
-
return false;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// -------- Opacity product memoization --------
|
|
860
|
-
|
|
861
|
-
const __localOpacityProductCache = new WeakMap();
|
|
862
|
-
const __opacityProductCache =
|
|
863
|
-
__getSharedWeakMapCache('__opacityProductCache') || __localOpacityProductCache;
|
|
864
|
-
|
|
865
|
-
function computeOpacityProduct(el) {
|
|
866
|
-
try {
|
|
867
|
-
if (!el || (typeof el !== 'object' && typeof el !== 'function')) return 1;
|
|
868
|
-
if (__opacityProductCache.has(el)) return __opacityProductCache.get(el);
|
|
869
|
-
|
|
870
|
-
let prod = 1;
|
|
871
|
-
let cur = el;
|
|
872
|
-
let guard = 0;
|
|
873
|
-
while (cur && guard++ < 200) {
|
|
874
|
-
if (cur.nodeType !== 1) {
|
|
875
|
-
cur = composedParent(cur);
|
|
876
|
-
continue;
|
|
877
|
-
}
|
|
878
|
-
const cs = __contrastComputedStyle(cur);
|
|
879
|
-
const o = clamp01(Number.parseFloat(cs && cs.opacity != null ? cs.opacity : '1'));
|
|
880
|
-
prod *= o;
|
|
881
|
-
cur = composedParent(cur);
|
|
882
|
-
if (prod <= 0) break;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
const out = clamp01(prod);
|
|
886
|
-
__opacityProductCache.set(el, out);
|
|
887
|
-
return out;
|
|
888
|
-
} catch (_e) {
|
|
889
|
-
return 1;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// -------- Effective foreground/background memoization --------
|
|
894
|
-
|
|
895
|
-
const __localEffectiveForegroundCache = new WeakMap();
|
|
896
|
-
const __effectiveForegroundCache =
|
|
897
|
-
__getSharedWeakMapCache('__effectiveForegroundCache') || __localEffectiveForegroundCache;
|
|
898
|
-
|
|
899
|
-
function computeEffectiveForeground(el) {
|
|
900
|
-
try {
|
|
901
|
-
if (el && __effectiveForegroundCache.has(el)) return __effectiveForegroundCache.get(el);
|
|
902
|
-
} catch (_e) {}
|
|
903
|
-
|
|
904
|
-
const cs = __contrastComputedStyle(el);
|
|
905
|
-
const c = parseCssColorToRgba(cs && cs.color);
|
|
906
|
-
if (!c) {
|
|
907
|
-
const out = { rgba: null, alpha: 0, opacityProduct: computeOpacityProduct(el) };
|
|
908
|
-
try {
|
|
909
|
-
if (el) __effectiveForegroundCache.set(el, out);
|
|
910
|
-
} catch (_e) {}
|
|
911
|
-
return out;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
const op = computeOpacityProduct(el);
|
|
915
|
-
const out = {
|
|
916
|
-
rgba: { r: c.r, g: c.g, b: c.b, a: clamp01(c.a * op) },
|
|
917
|
-
alpha: clamp01(c.a * op),
|
|
918
|
-
opacityProduct: op
|
|
919
|
-
};
|
|
920
|
-
try {
|
|
921
|
-
if (el) __effectiveForegroundCache.set(el, out);
|
|
922
|
-
} catch (_e) {}
|
|
923
|
-
return out;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
const __localEffectiveBackgroundCache = new WeakMap();
|
|
927
|
-
const __effectiveBackgroundCache =
|
|
928
|
-
__getSharedWeakMapCache('__effectiveBackgroundCache') || __localEffectiveBackgroundCache;
|
|
929
|
-
|
|
930
|
-
function __bgCacheKey(opts2) {
|
|
931
|
-
const contrast =
|
|
932
|
-
opts2 && opts2.contrast && typeof opts2.contrast === 'object' ? opts2.contrast : {};
|
|
933
|
-
const mode = contrast.mode === 'auditorAssist' ? 'auditorAssist' : 'strictConformance';
|
|
934
|
-
const rootCanvasFallback =
|
|
935
|
-
typeof contrast.rootCanvasFallback === 'string' && contrast.rootCanvasFallback.trim()
|
|
936
|
-
? contrast.rootCanvasFallback.trim()
|
|
937
|
-
: '#ffffff';
|
|
938
|
-
|
|
939
|
-
// Cache key must include any input that can affect computed background.
|
|
940
|
-
// Note: we only cache when collectStack is false.
|
|
941
|
-
return `${mode}|${rootCanvasFallback}`;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
function computeEffectiveBackground(el, opts2) {
|
|
945
|
-
const __bgKey = __bgCacheKey(opts2);
|
|
946
|
-
const __collectStack = !!(opts2 && opts2.collectStack);
|
|
947
|
-
|
|
948
|
-
// Only cache when stack collection is off
|
|
949
|
-
if (!__collectStack) {
|
|
950
|
-
try {
|
|
951
|
-
if (el && __effectiveBackgroundCache.has(el)) {
|
|
952
|
-
const m = __effectiveBackgroundCache.get(el);
|
|
953
|
-
if (m && typeof m.get === 'function' && m.has(__bgKey)) return m.get(__bgKey);
|
|
954
|
-
}
|
|
955
|
-
} catch (_e) {}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const contrast =
|
|
959
|
-
opts2 && opts2.contrast && typeof opts2.contrast === 'object' ? opts2.contrast : {};
|
|
960
|
-
const mode = contrast.mode === 'auditorAssist' ? 'auditorAssist' : 'strictConformance';
|
|
961
|
-
const rootCanvasFallback =
|
|
962
|
-
typeof contrast.rootCanvasFallback === 'string' && contrast.rootCanvasFallback.trim()
|
|
963
|
-
? contrast.rootCanvasFallback.trim()
|
|
964
|
-
: '#ffffff';
|
|
965
|
-
|
|
966
|
-
const collectStack = !!(opts2 && opts2.collectStack);
|
|
967
|
-
const stack = collectStack ? [] : null;
|
|
968
|
-
|
|
969
|
-
let acc = { r: 0, g: 0, b: 0, a: 0 };
|
|
970
|
-
let cur = el;
|
|
971
|
-
let guard = 0;
|
|
972
|
-
|
|
973
|
-
while (cur && guard++ < 200) {
|
|
974
|
-
if (cur.nodeType !== 1) {
|
|
975
|
-
cur = composedParent(cur);
|
|
976
|
-
continue;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
const cs = __contrastComputedStyle(cur);
|
|
980
|
-
const bg = parseCssColorToRgba(cs && cs.backgroundColor);
|
|
981
|
-
const op = clamp01(Number.parseFloat(cs && cs.opacity != null ? cs.opacity : '1'));
|
|
982
|
-
|
|
983
|
-
if (bg) {
|
|
984
|
-
const layer = { r: bg.r, g: bg.g, b: bg.b, a: clamp01(bg.a) };
|
|
985
|
-
if (collectStack) {
|
|
986
|
-
stack.push({
|
|
987
|
-
selector: __getSimpleSelectorCached(cur, (cur.tagName || '').toLowerCase() || 'html'),
|
|
988
|
-
bg: { r: layer.r, g: layer.g, b: layer.b, a: layer.a },
|
|
989
|
-
opacity: op
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
// This ancestor's own background sits BEHIND everything
|
|
993
|
-
// already accumulated from its descendants (acc is painted
|
|
994
|
-
// over it here).
|
|
995
|
-
acc = compositeRgba(acc, layer);
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// CSS opacity on an ancestor scales the *entire rendered
|
|
999
|
-
// subtree* (its own background plus everything already
|
|
1000
|
-
// accumulated from descendants) as one compositing group
|
|
1001
|
-
// against whatever is further out — not just that ancestor's
|
|
1002
|
-
// own background layer. Applying it here (after folding in
|
|
1003
|
-
// this ancestor's own bg) keeps that correct even when
|
|
1004
|
-
// accumulated alpha already reached 1 from an inner opaque
|
|
1005
|
-
// layer, which is why there is no longer an early
|
|
1006
|
-
// `acc.a >= 1` exit: a still-unvisited outer ancestor's
|
|
1007
|
-
// opacity can still reduce that alpha.
|
|
1008
|
-
if (op < 1) {
|
|
1009
|
-
acc = { r: acc.r, g: acc.g, b: acc.b, a: clamp01(acc.a * op) };
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
cur = composedParent(cur);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
let out;
|
|
1016
|
-
const allowAssumptions = mode === 'auditorAssist';
|
|
1017
|
-
|
|
1018
|
-
if (acc.a < 1) {
|
|
1019
|
-
if (allowAssumptions) {
|
|
1020
|
-
// If the root is not opaque, apply an explicit canvas fallback.
|
|
1021
|
-
const fb = parseCssColorToRgba(rootCanvasFallback) || { r: 255, g: 255, b: 255, a: 1 };
|
|
1022
|
-
const fbOpaque = { r: fb.r, g: fb.g, b: fb.b, a: 1 };
|
|
1023
|
-
|
|
1024
|
-
acc = compositeRgba(fbOpaque, acc);
|
|
1025
|
-
|
|
1026
|
-
out = {
|
|
1027
|
-
ok: true,
|
|
1028
|
-
rgba: { r: acc.r, g: acc.g, b: acc.b, a: 1 },
|
|
1029
|
-
alpha: 1,
|
|
1030
|
-
stack: stack || [],
|
|
1031
|
-
reasonCode: null,
|
|
1032
|
-
assumptionsApplied: ['ROOT_CANVAS_FALLBACK'],
|
|
1033
|
-
assumedRootCanvasColor: rootCanvasFallback
|
|
1034
|
-
};
|
|
1035
|
-
} else {
|
|
1036
|
-
out = {
|
|
1037
|
-
ok: false,
|
|
1038
|
-
rgba: acc,
|
|
1039
|
-
alpha: acc.a,
|
|
1040
|
-
stack: stack || [],
|
|
1041
|
-
reasonCode: 'BACKGROUND_NOT_OPAQUE_AT_ROOT'
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
} else {
|
|
1045
|
-
out = { ok: true, rgba: acc, alpha: acc.a, stack: stack || [], reasonCode: null };
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
if (!__collectStack && el) {
|
|
1049
|
-
try {
|
|
1050
|
-
let m = __effectiveBackgroundCache.get(el);
|
|
1051
|
-
if (!m) {
|
|
1052
|
-
m = new Map();
|
|
1053
|
-
__effectiveBackgroundCache.set(el, m);
|
|
1054
|
-
}
|
|
1055
|
-
m.set(__bgKey, out);
|
|
1056
|
-
} catch (_e) {}
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
return out;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// -------- Selector memoization --------
|
|
1063
|
-
|
|
1064
|
-
const __localSimpleSelectorCache = new WeakMap();
|
|
1065
|
-
const __simpleSelectorCache =
|
|
1066
|
-
__getSharedWeakMapCache('__simpleSelectorCache') || __localSimpleSelectorCache;
|
|
1067
|
-
|
|
1068
|
-
function __getSimpleSelectorCached(el, fallbackTag) {
|
|
1069
|
-
try {
|
|
1070
|
-
if (!el || el.nodeType !== 1) return '';
|
|
1071
|
-
if (__simpleSelectorCache.has(el)) return __simpleSelectorCache.get(el) || '';
|
|
1072
|
-
const s = buildSimpleSelector(el, fallbackTag);
|
|
1073
|
-
__simpleSelectorCache.set(el, s || '');
|
|
1074
|
-
return s || '';
|
|
1075
|
-
} catch (_e) {
|
|
1076
|
-
return '';
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
function classifyBackgroundImageValue(bgImageValue) {
|
|
1081
|
-
try {
|
|
1082
|
-
const v = bgImageValue == null ? '' : String(bgImageValue).trim();
|
|
1083
|
-
if (!v) return 'unknown';
|
|
1084
|
-
const s = v.toLowerCase();
|
|
1085
|
-
if (s === 'none') return 'unknown';
|
|
1086
|
-
|
|
1087
|
-
// Multiple layers can be comma-separated; we keep it simple + deterministic:
|
|
1088
|
-
// if any layer has url()/image-set() => image
|
|
1089
|
-
// if any layer has *gradient( => gradient
|
|
1090
|
-
const hasGradient = /gradient\s*\(/i.test(s);
|
|
1091
|
-
const hasUrl = /\burl\s*\(/i.test(s);
|
|
1092
|
-
const hasImageSet = /\bimage-set\s*\(/i.test(s);
|
|
1093
|
-
|
|
1094
|
-
const isImage = hasUrl || hasImageSet;
|
|
1095
|
-
const isGradient = hasGradient;
|
|
1096
|
-
|
|
1097
|
-
if (isImage && isGradient) return 'imageAndGradient';
|
|
1098
|
-
if (isImage) return 'image';
|
|
1099
|
-
if (isGradient) return 'gradient';
|
|
1100
|
-
|
|
1101
|
-
// Other background-image functions exist; treat as unknown rather than guessing.
|
|
1102
|
-
return 'unknown';
|
|
1103
|
-
} catch {
|
|
1104
|
-
return 'unknown';
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// -------- Computability blocker (memoized per element, per run) --------
|
|
1109
|
-
|
|
1110
|
-
const __localComputabilityBlockerCache = new WeakMap();
|
|
1111
|
-
const __computabilityBlockerCache =
|
|
1112
|
-
__getSharedWeakMapCache('__computabilityBlockerCache') || __localComputabilityBlockerCache;
|
|
1113
|
-
|
|
1114
|
-
function getComputabilityBlocker(el) {
|
|
1115
|
-
try {
|
|
1116
|
-
if (el && __computabilityBlockerCache.has(el)) return __computabilityBlockerCache.get(el);
|
|
1117
|
-
} catch (_e) {}
|
|
1118
|
-
|
|
1119
|
-
let cur = el;
|
|
1120
|
-
let guard = 0;
|
|
1121
|
-
// Once a closer ancestor's own background-color is confirmed fully
|
|
1122
|
-
// opaque (and that ancestor is itself free of blend-mode/filter),
|
|
1123
|
-
// it visually PAINTS OVER anything farther out -- a
|
|
1124
|
-
// background-image/gradient beyond that point can no longer affect
|
|
1125
|
-
// what's actually rendered behind el's text, so continuing to flag
|
|
1126
|
-
// it as a computability blocker is a false "not computable".
|
|
1127
|
-
// Confirmed via a minimal repro: solid black text on a
|
|
1128
|
-
// fully-opaque white <div>, itself sitting on a <body> with a
|
|
1129
|
-
// background-image, was reported cantTell even though the image is
|
|
1130
|
-
// 100% visually irrelevant to that text's rendered background —
|
|
1131
|
-
// and BACKGROUND_IMAGE_OR_GRADIENT was the dominant real-world
|
|
1132
|
-
// computability blocker (100% of cantTell occurrences sampled on
|
|
1133
|
-
// nasa.gov and en.wikipedia.org, 2026-08-01), so this single-layer
|
|
1134
|
-
// "solid card/nav/modal over a page-level hero image" pattern is
|
|
1135
|
-
// common enough to matter.
|
|
1136
|
-
//
|
|
1137
|
-
// This does NOT extend to mix-blend-mode/filter/backdrop-filter or
|
|
1138
|
-
// an ancestor's `opacity` — those are compositing-GROUP operations
|
|
1139
|
-
// applied to that ancestor's entire rendered subtree (including any
|
|
1140
|
-
// "opaque" layer inside it) before blending against whatever is
|
|
1141
|
-
// further out, so a closer opaque paint layer does not shield
|
|
1142
|
-
// against them the way it shields against a plain background-image.
|
|
1143
|
-
// Applying the same short-circuit there would risk a confidently
|
|
1144
|
-
// wrong pass — deliberately NOT done, matching this engine's
|
|
1145
|
-
// no-false-positives bar.
|
|
1146
|
-
let paintOccluded = false;
|
|
1147
|
-
|
|
1148
|
-
while (cur && guard++ < 200) {
|
|
1149
|
-
if (cur.nodeType !== 1) {
|
|
1150
|
-
cur = composedParent(cur);
|
|
1151
|
-
continue;
|
|
1152
|
-
}
|
|
1153
|
-
const cs = __contrastComputedStyle(cur);
|
|
1154
|
-
|
|
1155
|
-
if (__hasBlendModeEl(cur, cs)) {
|
|
1156
|
-
const out = {
|
|
1157
|
-
ok: false,
|
|
1158
|
-
reasonCode: 'MIX_BLEND_MODE',
|
|
1159
|
-
blockerSelector: __getSimpleSelectorCached(
|
|
1160
|
-
cur,
|
|
1161
|
-
(cur.tagName || '').toLowerCase() || 'html'
|
|
1162
|
-
),
|
|
1163
|
-
blockerProperty: 'mix-blend-mode',
|
|
1164
|
-
blockerValue: truncateCssValue(cs && cs.mixBlendMode, 80)
|
|
1165
|
-
};
|
|
1166
|
-
try {
|
|
1167
|
-
if (el) __computabilityBlockerCache.set(el, out);
|
|
1168
|
-
} catch (_e) {}
|
|
1169
|
-
return out;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
if (__hasFilterEl(cur, cs)) {
|
|
1173
|
-
const isFilter = cs && cs.filter && String(cs.filter).trim().toLowerCase() !== 'none';
|
|
1174
|
-
const out = {
|
|
1175
|
-
ok: false,
|
|
1176
|
-
reasonCode: 'BACKGROUND_FILTER_OR_BACKDROP_FILTER',
|
|
1177
|
-
blockerSelector: __getSimpleSelectorCached(
|
|
1178
|
-
cur,
|
|
1179
|
-
(cur.tagName || '').toLowerCase() || 'html'
|
|
1180
|
-
),
|
|
1181
|
-
blockerProperty: isFilter ? 'filter' : 'backdrop-filter',
|
|
1182
|
-
blockerValue: truncateCssValue((isFilter ? cs.filter : cs.backdropFilter) || '', 80)
|
|
1183
|
-
};
|
|
1184
|
-
try {
|
|
1185
|
-
if (el) __computabilityBlockerCache.set(el, out);
|
|
1186
|
-
} catch (_e) {}
|
|
1187
|
-
return out;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
if (!paintOccluded && __hasBackgroundImageOrGradientEl(cur, cs)) {
|
|
1191
|
-
const bgImg = (cs && cs.backgroundImage) || '';
|
|
1192
|
-
const out = {
|
|
1193
|
-
ok: false,
|
|
1194
|
-
reasonCode: 'BACKGROUND_IMAGE_OR_GRADIENT',
|
|
1195
|
-
blockerSelector: __getSimpleSelectorCached(
|
|
1196
|
-
cur,
|
|
1197
|
-
(cur.tagName || '').toLowerCase() || 'html'
|
|
1198
|
-
),
|
|
1199
|
-
blockerProperty: 'background-image',
|
|
1200
|
-
blockerValue: truncateCssValue(bgImg, 80),
|
|
1201
|
-
backgroundFillType: classifyBackgroundImageValue(bgImg)
|
|
1202
|
-
};
|
|
1203
|
-
try {
|
|
1204
|
-
if (el) __computabilityBlockerCache.set(el, out);
|
|
1205
|
-
} catch (_e) {}
|
|
1206
|
-
return out;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// An ANCESTOR (not el itself) with fractional opacity is treated
|
|
1210
|
-
// as a computability blocker rather than being folded into a
|
|
1211
|
-
// confident ratio. Group opacity uniformly scales an ancestor's
|
|
1212
|
-
// *entire* rendered subtree (its own background AND everything
|
|
1213
|
-
// already accumulated from descendants, including el's text)
|
|
1214
|
-
// when compositing against what's behind it — computing that
|
|
1215
|
-
// precisely for the foreground would require re-deriving the
|
|
1216
|
-
// text's rendered color the same way the background is folded
|
|
1217
|
-
// (rather than compositing a separately opacity-scaled
|
|
1218
|
-
// foreground against the fully-folded background, which
|
|
1219
|
-
// double-counts the ancestor's opacity). Rather than risk a
|
|
1220
|
-
// confidently wrong pass/fail from that mismatch, defer to
|
|
1221
|
-
// manual review. (el's own opacity, if any, does not trigger
|
|
1222
|
-
// this: it is already handled correctly by the existing
|
|
1223
|
-
// per-element opacity product used for the foreground.)
|
|
1224
|
-
if (cur !== el) {
|
|
1225
|
-
const ancestorOpacity = clamp01(
|
|
1226
|
-
Number.parseFloat(cs && cs.opacity != null ? cs.opacity : '1')
|
|
1227
|
-
);
|
|
1228
|
-
if (ancestorOpacity < 1) {
|
|
1229
|
-
const out = {
|
|
1230
|
-
ok: false,
|
|
1231
|
-
reasonCode: 'ANCESTOR_OPACITY',
|
|
1232
|
-
blockerSelector: __getSimpleSelectorCached(
|
|
1233
|
-
cur,
|
|
1234
|
-
(cur.tagName || '').toLowerCase() || 'html'
|
|
1235
|
-
),
|
|
1236
|
-
blockerProperty: 'opacity',
|
|
1237
|
-
blockerValue: truncateCssValue(String(cs && cs.opacity != null ? cs.opacity : '1'), 80)
|
|
1238
|
-
};
|
|
1239
|
-
try {
|
|
1240
|
-
if (el) __computabilityBlockerCache.set(el, out);
|
|
1241
|
-
} catch (_e) {}
|
|
1242
|
-
return out;
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// `cur` cleared every check above (no blend-mode/filter/
|
|
1247
|
-
// background-image/gradient of its own, and no reduced
|
|
1248
|
-
// opacity) -- if its own background-color also happens to be
|
|
1249
|
-
// fully opaque, it paints over everything farther out, so
|
|
1250
|
-
// suppress BACKGROUND_IMAGE_OR_GRADIENT for any ancestor beyond
|
|
1251
|
-
// this point (see the paintOccluded comment above the loop).
|
|
1252
|
-
if (!paintOccluded) {
|
|
1253
|
-
const ownBg = parseCssColorToRgba(cs && cs.backgroundColor);
|
|
1254
|
-
if (ownBg && clamp01(ownBg.a) >= 1) paintOccluded = true;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
cur = composedParent(cur);
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
const out = {
|
|
1261
|
-
ok: true,
|
|
1262
|
-
reasonCode: null,
|
|
1263
|
-
blockerSelector: '',
|
|
1264
|
-
blockerProperty: '',
|
|
1265
|
-
blockerValue: ''
|
|
1266
|
-
};
|
|
1267
|
-
try {
|
|
1268
|
-
if (el) __computabilityBlockerCache.set(el, out);
|
|
1269
|
-
} catch (_e) {}
|
|
1270
|
-
return out;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
return {
|
|
1274
|
-
clamp01,
|
|
1275
|
-
clamp255,
|
|
1276
|
-
round2,
|
|
1277
|
-
rgbaToString,
|
|
1278
|
-
parsePx,
|
|
1279
|
-
normalizeFontWeight,
|
|
1280
|
-
isLargeText,
|
|
1281
|
-
requiredRatio,
|
|
1282
|
-
parseCssColorToRgba,
|
|
1283
|
-
compositeRgba,
|
|
1284
|
-
relativeLuminance,
|
|
1285
|
-
contrastRatio,
|
|
1286
|
-
toHex2,
|
|
1287
|
-
rgbToHex,
|
|
1288
|
-
pxToPt,
|
|
1289
|
-
fontWeightLabel,
|
|
1290
|
-
hasBackgroundImageOrGradient,
|
|
1291
|
-
hasBlendMode,
|
|
1292
|
-
hasFilter,
|
|
1293
|
-
computeOpacityProduct,
|
|
1294
|
-
computeEffectiveForeground,
|
|
1295
|
-
computeEffectiveBackground,
|
|
1296
|
-
getComputabilityBlocker,
|
|
1297
|
-
getTextScan,
|
|
1298
|
-
isInactiveUiComponent
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
module.exports = { createContrastHelpers };
|