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