@surea11y/core 1.2.0 → 1.4.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 (168) hide show
  1. package/CHANGELOG.md +81 -7
  2. package/LICENSE +373 -21
  3. package/README.md +175 -35
  4. package/bin/surea11y-core.js +20 -0
  5. package/docs/API_STABILITY.md +27 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/ENGINE_OPTIONS.md +2 -0
  9. package/docs/I18N.md +12 -9
  10. package/docs/INTEGRATION.md +19 -1
  11. package/docs/LIMITATIONS.md +1 -1
  12. package/docs/OUTPUT_SCHEMA.md +1 -1
  13. package/docs/REPORT.md +1 -1
  14. package/docs/RULE_CATALOG.md +1 -1
  15. package/docs/SARIF.md +59 -0
  16. package/package.json +63 -18
  17. package/src/baseline.js +0 -0
  18. package/src/checks/automatic/area-alt-present.js +63 -31
  19. package/src/checks/automatic/aria-allowed-attr.js +204 -80
  20. package/src/checks/automatic/aria-allowed-role.js +23 -7
  21. package/src/checks/automatic/aria-braille-equivalent.js +34 -10
  22. package/src/checks/automatic/aria-conditional-attr.js +32 -14
  23. package/src/checks/automatic/aria-deprecated-role.js +26 -11
  24. package/src/checks/automatic/aria-hidden-body.js +48 -23
  25. package/src/checks/automatic/aria-hidden-focus.js +420 -66
  26. package/src/checks/automatic/aria-prohibited-attr.js +327 -60
  27. package/src/checks/automatic/aria-prohibited-children.js +111 -103
  28. package/src/checks/automatic/aria-required-attr.js +29 -15
  29. package/src/checks/automatic/aria-required-children.js +44 -24
  30. package/src/checks/automatic/aria-required-parent.js +64 -35
  31. package/src/checks/automatic/aria-role-name-present.js +49 -21
  32. package/src/checks/automatic/aria-roles-valid.js +24 -12
  33. package/src/checks/automatic/aria-valid-attr-value.js +46 -22
  34. package/src/checks/automatic/aria-valid-attr.js +19 -5
  35. package/src/checks/automatic/autocomplete-valid.js +76 -16
  36. package/src/checks/automatic/avoid-inline-spacing.js +23 -8
  37. package/src/checks/automatic/binary-control-name-present.js +62 -50
  38. package/src/checks/automatic/button-name-present.js +54 -24
  39. package/src/checks/automatic/bypass-blocks-present.js +51 -32
  40. package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
  41. package/src/checks/automatic/combobox-name-present.js +40 -45
  42. package/src/checks/automatic/contrast-computable.js +363 -341
  43. package/src/checks/automatic/contrast-enhanced.js +489 -466
  44. package/src/checks/automatic/contrast-minimum.js +488 -465
  45. package/src/checks/automatic/css-orientation-lock.js +51 -35
  46. package/src/checks/automatic/definition-list-children-valid.js +46 -25
  47. package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
  48. package/src/checks/automatic/dialog-name-present.js +47 -85
  49. package/src/checks/automatic/dlitem-parent-valid.js +25 -8
  50. package/src/checks/automatic/duplicate-id-aria.js +28 -9
  51. package/src/checks/automatic/embed-text-alternative-present.js +88 -35
  52. package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
  53. package/src/checks/automatic/form-control-single-label.js +50 -14
  54. package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
  55. package/src/checks/automatic/iframe-focusable-content.js +265 -22
  56. package/src/checks/automatic/iframe-name-present.js +33 -9
  57. package/src/checks/automatic/iframe-title-unique.js +32 -9
  58. package/src/checks/automatic/img-alt-present.js +54 -52
  59. package/src/checks/automatic/input-image-alt-present.js +141 -112
  60. package/src/checks/automatic/label-in-name.js +65 -41
  61. package/src/checks/automatic/language-page-present.js +111 -109
  62. package/src/checks/automatic/link-in-text-block.js +61 -19
  63. package/src/checks/automatic/link-name-present.js +47 -14
  64. package/src/checks/automatic/list-children-valid.js +40 -33
  65. package/src/checks/automatic/listbox-name-present.js +41 -19
  66. package/src/checks/automatic/listitem-parent-valid.js +48 -13
  67. package/src/checks/automatic/menuitem-name-present.js +41 -61
  68. package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
  69. package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
  70. package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
  71. package/src/checks/automatic/meter-name-present.js +40 -36
  72. package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
  73. package/src/checks/automatic/object-text-alternative-present.js +93 -39
  74. package/src/checks/automatic/option-name-present.js +40 -21
  75. package/src/checks/automatic/page-title-present.js +19 -6
  76. package/src/checks/automatic/progressbar-name-present.js +49 -44
  77. package/src/checks/automatic/role-img-alt-present.js +211 -159
  78. package/src/checks/automatic/searchbox-name-present.js +41 -19
  79. package/src/checks/automatic/server-side-image-map-absent.js +27 -11
  80. package/src/checks/automatic/slider-name-present.js +42 -47
  81. package/src/checks/automatic/spinbutton-name-present.js +41 -19
  82. package/src/checks/automatic/summary-name-present.js +39 -17
  83. package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
  84. package/src/checks/automatic/svg-text-alternative-present.js +262 -230
  85. package/src/checks/automatic/tab-name-present.js +39 -60
  86. package/src/checks/automatic/table-headers-attr-valid.js +27 -10
  87. package/src/checks/automatic/table-th-has-data-cells.js +24 -8
  88. package/src/checks/automatic/target-size-minimum.js +123 -48
  89. package/src/checks/automatic/td-has-header.js +53 -12
  90. package/src/checks/automatic/textbox-name-present.js +41 -19
  91. package/src/checks/automatic/tooltip-name-present.js +39 -18
  92. package/src/checks/automatic/treeitem-name-present.js +40 -21
  93. package/src/checks/automatic/valid-lang.js +22 -6
  94. package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
  95. package/src/checks/manual/accesskeys-manual.js +17 -6
  96. package/src/checks/manual/area-alt-decorative-manual.js +194 -193
  97. package/src/checks/manual/area-alt-quality-manual.js +184 -141
  98. package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
  99. package/src/checks/manual/aria-text-manual.js +20 -11
  100. package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
  101. package/src/checks/manual/css-hidden-focus.js +375 -169
  102. package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
  103. package/src/checks/manual/empty-heading-manual.js +41 -24
  104. package/src/checks/manual/empty-table-header-manual.js +69 -31
  105. package/src/checks/manual/focus-order-semantics-manual.js +60 -13
  106. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
  107. package/src/checks/manual/heading-order-manual.js +50 -8
  108. package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
  109. package/src/checks/manual/image-redundant-alt-manual.js +38 -8
  110. package/src/checks/manual/img-alt-decorative-manual.js +133 -96
  111. package/src/checks/manual/img-alt-quality-manual.js +178 -127
  112. package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
  113. package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
  114. package/src/checks/manual/label-title-only-manual.js +44 -28
  115. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
  116. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
  117. package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
  118. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
  119. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
  120. package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
  121. package/src/checks/manual/landmark-one-main-manual.js +38 -43
  122. package/src/checks/manual/landmark-unique-manual.js +78 -67
  123. package/src/checks/manual/link-name-quality-manual.js +45 -12
  124. package/src/checks/manual/media-transcript-present-manual.js +37 -22
  125. package/src/checks/manual/meta-viewport-large-manual.js +19 -6
  126. package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
  127. package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
  128. package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
  129. package/src/checks/manual/p-as-heading-manual.js +24 -7
  130. package/src/checks/manual/page-has-heading-one-manual.js +42 -32
  131. package/src/checks/manual/page-title-patterns-manual.js +80 -50
  132. package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
  133. package/src/checks/manual/region-manual.js +244 -60
  134. package/src/checks/manual/scope-attr-valid-manual.js +13 -4
  135. package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
  136. package/src/checks/manual/skip-link-manual.js +42 -18
  137. package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
  138. package/src/checks/manual/tabindex-manual.js +13 -4
  139. package/src/checks/manual/table-duplicate-name-manual.js +22 -11
  140. package/src/checks/manual/table-fake-caption-manual.js +48 -10
  141. package/src/checks/manual/video-caption-manual.js +17 -4
  142. package/src/checks/manual-review.js +58 -12
  143. package/src/core.js +41705 -29650
  144. package/src/index.js +2 -0
  145. package/src/report.js +109 -47
  146. package/src/sarif.js +190 -0
  147. package/surea11y.browser.js +37774 -0
  148. package/bin/core.js +0 -348
  149. package/docs/CLI.md +0 -75
  150. package/src/catalogs/composites.wcag.js +0 -490
  151. package/src/checks/rules-and-tags.full.csv +0 -19
  152. package/src/checks/rules-and-tags.full.json +0 -259
  153. package/src/core/aria-helpers.js +0 -970
  154. package/src/core/contrast-helpers.js +0 -1147
  155. package/src/core/dom-helpers.js +0 -4235
  156. package/src/core/dom-runner.js +0 -671
  157. package/src/core/frame-messaging.js +0 -210
  158. package/src/core/frame-scan.js +0 -178
  159. package/src/core/rollup-composites.js +0 -135
  160. package/src/core/rule-meta.js +0 -159
  161. package/src/coverage/wcag-facets.js +0 -1079
  162. package/src/coverage/wcag-version-map.js +0 -84
  163. package/src/i18n/en.js +0 -923
  164. package/src/i18n/fr.js +0 -844
  165. package/src/policy/contracts.js +0 -18
  166. package/src/policy/resolvePolicy.js +0 -55
  167. package/src/policy/schemas/engine-options.schema.json +0 -103
  168. package/src/policy/schemas/policy-contract.schema.json +0 -40
@@ -1,1147 +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') return existing;
35
- const wm = new WeakMap();
36
- sc[propName] = wm;
37
- return wm;
38
- } catch (_e) {
39
- return null;
40
- }
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
- }
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
- }
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
- }
111
- }
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';
126
- }
127
-
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
- }
210
- }
211
-
212
- return 'styleOnly';
213
- }
214
-
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;
238
- }
239
- return false;
240
- }
241
-
242
- function getTextScan(ctx, helpers, engineOptions) {
243
- 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;
377
- } catch {
378
- return { eligibleTextCount: 0, elements: [], visibilityMode: 'styleOnly' };
379
- }
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
-
390
- function rgbToHex(rgb) {
391
- try {
392
- if (!rgb || typeof rgb !== 'object') return '';
393
- return '#' + toHex2(rgb.r) + toHex2(rgb.g) + toHex2(rgb.b);
394
- } catch {
395
- return '';
396
- }
397
- }
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;
446
- }
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;
456
- }
457
-
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
- }
466
-
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
- }
472
-
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;
489
- }
490
-
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;
532
- }
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
- }
575
- }
576
-
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 };
611
- }
612
-
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
- 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
- }
647
- } catch (_e) {}
648
-
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);
656
-
657
- const out = __parseCssColorToRgbaUncached(key);
658
- __colorParseCache.set(key, out);
659
- return out;
660
- }
661
-
662
- // -------- Color math --------
663
-
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 };
667
-
668
- const as = clamp01(s.a);
669
- const ad = clamp01(d.a);
670
-
671
- const outA = as + ad * (1 - as);
672
- if (outA <= 0) return { r: 0, g: 0, b: 0, a: 0 };
673
-
674
- const rs = clamp255(s.r);
675
- const gs = clamp255(s.g);
676
- const bs = clamp255(s.b);
677
-
678
- const rd = clamp255(d.r);
679
- const gd = clamp255(d.g);
680
- const bd = clamp255(d.b);
681
-
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;
685
-
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
- }
693
-
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
- }
699
-
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;
705
- }
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);
713
- }
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) + '...';
720
- }
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
- }
729
- }
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
- }
738
- }
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
- }
750
- }
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;
761
- }
762
- }
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
- }
774
- }
775
-
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;
785
- }
786
- }
787
-
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;
818
- }
819
- }
820
-
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
845
- };
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}`;
865
- }
866
-
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) {}
973
- }
974
-
975
- return out;
976
- }
977
-
978
-
979
- // -------- Selector memoization --------
980
-
981
- const __localSimpleSelectorCache = new WeakMap();
982
- const __simpleSelectorCache = __getSharedWeakMapCache('__simpleSelectorCache') || __localSimpleSelectorCache;
983
-
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
- }
994
- }
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
- }
1022
- }
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) {
1031
- try {
1032
- if (el && __computabilityBlockerCache.has(el)) return __computabilityBlockerCache.get(el);
1033
- } catch (_e) {}
1034
-
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
- }
1112
-
1113
- const out = { ok: true, reasonCode: null, blockerSelector: '', blockerProperty: '', blockerValue: '' };
1114
- try { if (el) __computabilityBlockerCache.set(el, out); } catch (_e) {}
1115
- return out;
1116
- }
1117
-
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
1144
- };
1145
- }
1146
-
1147
- module.exports = { createContrastHelpers };