@surea11y/core 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -0
- package/docs/ENGINE_OPTIONS.md +31 -3
- package/docs/TROUBLESHOOTING.md +1 -1
- package/package.json +2 -1
- package/src/core/dom-helpers.js +48 -16
- package/src/core/dom-runner.js +17 -0
- package/src/core.js +195 -48
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,10 @@ All notable changes to this project are documented here, in [Keep a Changelog](h
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.1.0] - 2026-07-28
|
|
8
|
+
|
|
7
9
|
### Added
|
|
10
|
+
- `engineOptions.rules[ruleId].excludeSelectors`: rule-scoped exclusions, narrowing candidates for exactly one rule on top of (never instead of) the existing global `excludeSelectors`. Resolves the class of false positive where one rule misfires on a component (e.g. Angular Material's `mat-select` tripping `aria-required-children`) while every other rule still needs to see it. Filtering happens upstream of each rule's own outcome decision, so no rule files changed. See `docs/ENGINE_OPTIONS.md`'s "Rule-scoped `excludeSelectors`" section.
|
|
8
11
|
- Completed French (`fr`) localization: translated the 313 remaining `src/i18n/fr.js` keys, bringing French to full parity with English (600/600 keys, up from 287/600). Verified against a live scan (`locale: 'fr'`) and confirmed no key/placeholder mismatches against `src/i18n/en.js`. Landmark terminology uses "point de repère" per MDN's French ARIA documentation.
|
|
9
12
|
|
|
10
13
|
### Fixed
|
package/docs/ENGINE_OPTIONS.md
CHANGED
|
@@ -95,7 +95,9 @@ const engineOptions = {
|
|
|
95
95
|
},
|
|
96
96
|
|
|
97
97
|
rules: {
|
|
98
|
-
'some-rule-id': {
|
|
98
|
+
'some-rule-id': {
|
|
99
|
+
excludeSelectors: ['.some-noisy-widget'] // narrows candidates for THIS rule only — see note
|
|
100
|
+
}
|
|
99
101
|
},
|
|
100
102
|
|
|
101
103
|
probes: { /* optional host-supplied evidence, see note */ },
|
|
@@ -113,18 +115,44 @@ const engineOptions = {
|
|
|
113
115
|
|---|---|
|
|
114
116
|
| `locale` | Any string; resolution is per-string with graceful fallback (requested locale → `en` → the rule's literal English fallback text), so a partially-translated locale never produces missing text. See [`I18N.md`](./I18N.md) for current locale coverage. |
|
|
115
117
|
| `includeShadowDom` | Default `true`: rules using `helpers.queryAllSmart` traverse into open shadow roots. Set `false` to scan only the light DOM. Closed shadow roots are never reachable either way (no DOM API exposes them). |
|
|
116
|
-
| `excludeSelectors` | Elements matching any of these selectors (and their descendants) are skipped entirely — useful for cookie banners, third-party embeds, or known-noisy widgets you don't control. |
|
|
118
|
+
| `excludeSelectors` | Elements matching any of these selectors (and their descendants) are skipped entirely, for **every** rule — useful for cookie banners, third-party embeds, or known-noisy widgets you don't control. To exclude something from just one specific rule instead, use `rules[ruleId].excludeSelectors` below. |
|
|
117
119
|
| `timestamp` | Passed straight through to the result's top-level `timestamp` field; the engine does not generate one itself (deterministic-by-design). |
|
|
118
120
|
| `contrast.mode` | `strictConformance` (default): contrast rules stay silent (`notApplicable`/skip) whenever the true rendered background isn't confidently computable, to protect against false `fail`s. `auditorAssist`: trades some of that safety margin for more findings, intended for a human auditor who will double-check flagged cases, not for unattended CI gating. |
|
|
119
121
|
| `contrast.rootCanvasFallback` | The assumed page background color when it's not computable at all — only matters in `auditorAssist` mode. |
|
|
120
122
|
| `visibilityMode` | Controls how strict the three contrast rules (`contrast-minimum`, `contrast-enhanced`, `contrast-computable`) are about deciding a text node is actually eligible to check. **Not read by any other rule.** `'styleOnly'` (default): eligibility is CSS-only — `display`, `visibility`, `opacity`, ancestor-hiding, etc. `'styleAndGeometry'`: adds real layout checks (`getClientRects()`/`getBoundingClientRect()`) on top of that — text with no client rects, or zero width/height, is excluded too. Reach for `'styleAndGeometry'` when running under a real browser/Playwright-Puppeteer (`runa11yCoreInPage`) and you want contrast findings to reflect actual rendered layout rather than just computed style; under plain jsdom (`runDomRulesInPage`) there's no real layout engine, so `'styleAndGeometry'` mostly just adds `getBoundingClientRect()` zero-size checks, not true clipping/overflow detection — see [`LIMITATIONS.md`](./LIMITATIONS.md). |
|
|
121
123
|
| `policyContract` / `policy` | See [`POLICY.md`](./POLICY.md) — controls which outcomes/confidence values are allowed and whether manual rules' would-be `fail`s get coerced to `cantTell`. |
|
|
122
124
|
| `output.includeSelector` / `.includeHtml` | Only affects the small number of rules (currently 4 of 125) that rely on the engine's automatic selector/HTML fill-in rather than building their own — most rules set `selector`/`html` themselves inside `runInPage` and are unaffected by this option. Not a reliable way to strip selectors/HTML from all output. |
|
|
123
|
-
| `rules[ruleId]` | Passed through to that rule as `ctx.config
|
|
125
|
+
| `rules[ruleId]` | Passed through to that rule as `ctx.config`, and — for `excludeSelectors` specifically — read by the engine itself before the rule ever runs. See "Rule-scoped `excludeSelectors`" below. Any other key is passthrough only: **no shipped rule currently reads `ctx.config`** for anything besides `excludeSelectors`. |
|
|
124
126
|
| `probes` | An optional, JSON-safe evidence object your host application can supply (depth- and size-capped by the engine before rules see it, via `ctx.inputs.probes`) — for future rules that might accept externally-supplied signals (e.g. real layout measurements a static DOM scan can't compute itself). Not consumed by any current rule. |
|
|
125
127
|
| `perfStats` / `profileRules` | Debug-only. `perfStats: true` returns internal counters on the result's `perfStats` field; `profileRules: true` additionally adds a per-rule timing breakdown. Shape is not part of the stable output contract — don't build on it. |
|
|
126
128
|
| `pingWaitTime` / `frameWaitTime` | Only read by `runa11yCoreAcrossFrames` (see [`INTEGRATION.md`](./INTEGRATION.md#cross-frame-scanning-including-cross-origin)) — how long to wait for a child frame to answer a ping (default `500`ms) and a full run request (default `60000`ms) before treating it as unreachable. Ignored by `runDomRulesInPage`/`runa11yCoreInPage`. |
|
|
127
129
|
|
|
130
|
+
### Rule-scoped `excludeSelectors`
|
|
131
|
+
|
|
132
|
+
The top-level `excludeSelectors` applies to *every* rule — there's no way to exclude an element from just one rule while still running every other rule against it. `rules[ruleId].excludeSelectors` fills that gap: it narrows candidates for **that one rule only**, on top of (never instead of) the global list.
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
const engineOptions = {
|
|
136
|
+
excludeSelectors: ['#cookie-banner'], // applies to every rule, as always
|
|
137
|
+
rules: {
|
|
138
|
+
'aria-required-children': {
|
|
139
|
+
excludeSelectors: ['mat-select', 'mat-stepper', 'mat-horizontal-stepper', 'mat-vertical-stepper']
|
|
140
|
+
},
|
|
141
|
+
'aria-allowed-attr': {
|
|
142
|
+
excludeSelectors: ['mat-progress-spinner']
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Why you'd want this: Angular Material's `<mat-select>` builds its internal ARIA structure in a way that trips a false positive on `aria-required-children` specifically, even though the component is otherwise fine. With only the global `excludeSelectors`, the only way to silence that false positive is `excludeSelectors: ['mat-select']` — which also hides `mat-select` from *every other rule*, including `color-contrast` and `aria-allowed-attr`, silently dropping real coverage those checks never had a problem with. The example above keeps `mat-select` fully visible to every rule except the one that misfires on it.
|
|
149
|
+
|
|
150
|
+
Effective exclusions for a given rule are the **union** of the global list and that rule's own list — an element matching either is dropped from that rule's candidates. A rule whose only would-be-failing elements are all excluded this way reports `outcome: 'pass'` or `'notApplicable'` (matching that rule's own no-candidates convention), with `occurrences: []` — never `outcome: 'fail'` with an empty `occurrences` array, since that exact shape is reserved elsewhere in the schema to mean "this rule threw" (see [`OUTPUT_SCHEMA.md`](./OUTPUT_SCHEMA.md)).
|
|
151
|
+
|
|
152
|
+
Accepts the same forms as the global option: an array (`['mat-select', 'mat-stepper']`) or a comma-separated string (`'mat-select, mat-stepper'`).
|
|
153
|
+
|
|
154
|
+
> If you're using a binding package (`@surea11y/binding-base` and its Playwright/Puppeteer wrappers), check that binding's own README for whether its `.exclude()` builder method has a rule-scoped form yet — this is an `engineOptions` shape documented here at the engine level; not every binding has picked it up.
|
|
155
|
+
|
|
128
156
|
## Recipes — composing options for real scenarios
|
|
129
157
|
|
|
130
158
|
The reference above documents each option in isolation. These combine several at once, for scenarios you're likely to actually hit.
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -29,7 +29,7 @@ By design, this engine never enumerates the elements a rule *passed* — only th
|
|
|
29
29
|
Two common causes, in order of likelihood:
|
|
30
30
|
|
|
31
31
|
1. **The element is excluded from the accessibility tree** — `aria-hidden="true"`, `display: none`, `visibility: hidden`, `hidden`, or an `inert` ancestor. Most rules deliberately skip content that's already invisible to assistive technology (checking a hidden element would be meaningless, and could produce a misleading `fail` on content no user encounters). Some rules explicitly opt out of this gating when it wouldn't make sense to (e.g. `no-autoplay-audio` — hidden audio still plays sound) — check the specific rule's file header comment (`@applicability`) in `src/checks/`.
|
|
32
|
-
2. **`excludeSelectors`** — if you've configured this (directly or inherited from a shared config), confirm the element in question isn't matched by it.
|
|
32
|
+
2. **`excludeSelectors`** — if you've configured this (directly or inherited from a shared config), confirm the element in question isn't matched by it. Remember this can also be scoped to a single rule via `engineOptions.rules[ruleId].excludeSelectors` (see [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md#rule-scoped-excludeselectors)) — if a rule you expect to fire keeps coming back `notApplicable`/`pass` for one element only, check whether that rule specifically has its own exclude list configured, not just the global one.
|
|
33
33
|
|
|
34
34
|
## "Does a clean scan (`pass` everywhere) mean the page is WCAG conformant?"
|
|
35
35
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@surea11y/core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Lightweight DOM rules accessibility core with modular rules.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "node scripts/build-core.js",
|
|
40
|
+
"pretest": "playwright install chromium",
|
|
40
41
|
"test": "npm run build && node scripts/run-tests.js",
|
|
41
42
|
"test:contrast-helpers": "node tests/contrast-helpers.test.js",
|
|
42
43
|
"helpers-perf-bench": "node --expose-gc scripts/dom-helpers-perf-bench.js",
|
package/src/core/dom-helpers.js
CHANGED
|
@@ -117,14 +117,35 @@ function createDomHelpers(opts) {
|
|
|
117
117
|
const includeShadowDom = !(opts && opts.includeShadowDom === false);
|
|
118
118
|
const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
|
|
119
119
|
|
|
120
|
+
// Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
|
|
121
|
+
// by dom-runner.js immediately before invoking each rule's applicability/
|
|
122
|
+
// run function via __setActiveRuleExcludeSelectors(). Safe as mutable
|
|
123
|
+
// closure state because rule execution is synchronous and single-rule-
|
|
124
|
+
// at-a-time: exactly one rule's excludes are ever "active" at once.
|
|
125
|
+
var __activeRuleExcludeSelectors = [];
|
|
126
|
+
|
|
127
|
+
function __getEffectiveExcludeSelectors() {
|
|
128
|
+
return __activeRuleExcludeSelectors.length
|
|
129
|
+
? excludeSelectors.concat(__activeRuleExcludeSelectors)
|
|
130
|
+
: excludeSelectors;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function __setActiveRuleExcludeSelectors(list) {
|
|
134
|
+
__activeRuleExcludeSelectors = normalizeSelectorList(list);
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
// Selector-related caches (selector uniqueness index, per-element built
|
|
121
|
-
// selector strings) depend on includeShadowDom/
|
|
122
|
-
// those change which elements are considered when checking
|
|
123
|
-
// The underlying storage is shared across createDomHelpers()
|
|
124
|
-
// the same window/document (see __domSharedCache below), so a
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
138
|
+
// selector strings) depend on includeShadowDom/the effective exclude
|
|
139
|
+
// list, since those change which elements are considered when checking
|
|
140
|
+
// uniqueness. The underlying storage is shared across createDomHelpers()
|
|
141
|
+
// calls on the same window/document (see __domSharedCache below), so a
|
|
142
|
+
// run -- or a rule with its own rule-scoped excludes -- must not
|
|
143
|
+
// read/write another run/rule's cached selectors. This key partitions
|
|
144
|
+
// those caches per effective option set; recomputed per call (not a
|
|
145
|
+
// constant) since the effective list changes as the active rule changes.
|
|
146
|
+
function __getSelectorOptsKey() {
|
|
147
|
+
return (includeShadowDom ? 'sd1' : 'sd0') + '|' + __getEffectiveExcludeSelectors().slice().sort().join(',');
|
|
148
|
+
}
|
|
128
149
|
|
|
129
150
|
// -------------------------------------------------------------------------
|
|
130
151
|
// Per-run shared caches (DOM helpers)
|
|
@@ -886,9 +907,10 @@ function createDomHelpers(opts) {
|
|
|
886
907
|
|
|
887
908
|
|
|
888
909
|
function isExcluded(el) {
|
|
889
|
-
|
|
910
|
+
const eff = __getEffectiveExcludeSelectors();
|
|
911
|
+
if (!eff.length || !el || !el.closest) return false;
|
|
890
912
|
try {
|
|
891
|
-
return
|
|
913
|
+
return eff.some((sel) => !!el.closest(sel));
|
|
892
914
|
} catch {
|
|
893
915
|
return false;
|
|
894
916
|
}
|
|
@@ -983,8 +1005,10 @@ function createDomHelpers(opts) {
|
|
|
983
1005
|
if (!scope || !scope.querySelectorAll) return [];
|
|
984
1006
|
|
|
985
1007
|
// Cache shadow root discovery per root to avoid repeated querySelectorAll('*') walks.
|
|
986
|
-
// IMPORTANT: do not cache when
|
|
987
|
-
|
|
1008
|
+
// IMPORTANT: do not cache when the effective exclude list (global
|
|
1009
|
+
// ∪ active rule-scoped excludes) is non-empty -- different rules
|
|
1010
|
+
// may have different effective lists and must not share results.
|
|
1011
|
+
if (!__getEffectiveExcludeSelectors().length && __shadowRootsByRoot) {
|
|
988
1012
|
try {
|
|
989
1013
|
const cached = __shadowRootsByRoot.get(scope);
|
|
990
1014
|
if (cached) {
|
|
@@ -1059,7 +1083,7 @@ function createDomHelpers(opts) {
|
|
|
1059
1083
|
|
|
1060
1084
|
function queryAllSmart(sel) {
|
|
1061
1085
|
const list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
|
|
1062
|
-
return
|
|
1086
|
+
return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
|
|
1063
1087
|
}
|
|
1064
1088
|
|
|
1065
1089
|
// -------------------------------------------------------------------------
|
|
@@ -1093,10 +1117,11 @@ function createDomHelpers(opts) {
|
|
|
1093
1117
|
function __getSelectorCacheForOpts() {
|
|
1094
1118
|
if (!__selectorCache) return null;
|
|
1095
1119
|
try {
|
|
1096
|
-
|
|
1120
|
+
const key = __getSelectorOptsKey();
|
|
1121
|
+
let wm = __selectorCache.get(key);
|
|
1097
1122
|
if (!(wm instanceof WeakMap)) {
|
|
1098
1123
|
wm = new WeakMap();
|
|
1099
|
-
__selectorCache.set(
|
|
1124
|
+
__selectorCache.set(key, wm);
|
|
1100
1125
|
}
|
|
1101
1126
|
return wm;
|
|
1102
1127
|
} catch {
|
|
@@ -3507,7 +3532,8 @@ function createDomHelpers(opts) {
|
|
|
3507
3532
|
return createSelectorUniqIndex();
|
|
3508
3533
|
}
|
|
3509
3534
|
|
|
3510
|
-
const
|
|
3535
|
+
const key = __getSelectorOptsKey();
|
|
3536
|
+
const cached = perScope.get(key);
|
|
3511
3537
|
if (cached) {
|
|
3512
3538
|
__perfInc('uniqIndex.hit');
|
|
3513
3539
|
return cached;
|
|
@@ -3516,7 +3542,7 @@ function createDomHelpers(opts) {
|
|
|
3516
3542
|
__perfInc('uniqIndex.miss');
|
|
3517
3543
|
const idx = createSelectorUniqIndex();
|
|
3518
3544
|
try {
|
|
3519
|
-
perScope.set(
|
|
3545
|
+
perScope.set(key, idx);
|
|
3520
3546
|
} catch { /* ignore */
|
|
3521
3547
|
}
|
|
3522
3548
|
__perfInc('uniqIndex.build');
|
|
@@ -4026,6 +4052,12 @@ function createDomHelpers(opts) {
|
|
|
4026
4052
|
isAccTreeEligible,
|
|
4027
4053
|
isDomVisibleEligible,
|
|
4028
4054
|
|
|
4055
|
+
// Engine-internal: sets which rule's rule-scoped excludeSelectors
|
|
4056
|
+
// (engineOptions.rules[ruleId].excludeSelectors) are currently in
|
|
4057
|
+
// effect. Called by dom-runner.js before each rule invocation, not
|
|
4058
|
+
// intended for use by rule implementations.
|
|
4059
|
+
__setActiveRuleExcludeSelectors,
|
|
4060
|
+
|
|
4029
4061
|
// Eligibility info wrapper
|
|
4030
4062
|
getEligibilityInfo,
|
|
4031
4063
|
|
package/src/core/dom-runner.js
CHANGED
|
@@ -242,6 +242,15 @@ function runCore(pageUrl, contextSelector, engineOptions, runOnly, CHECK_DEFS, R
|
|
|
242
242
|
? engineOptionsResolved.rules[defResolved.ruleId]
|
|
243
243
|
: null;
|
|
244
244
|
|
|
245
|
+
// Rule-scoped excludeSelectors (engineOptions.rules[ruleId].excludeSelectors)
|
|
246
|
+
// apply on top of the global excludeSelectors for exactly this rule's
|
|
247
|
+
// applicability check + run, then are cleared once this rule is done.
|
|
248
|
+
// Safe because rule execution below is synchronous and one rule at a
|
|
249
|
+
// time -- sharedHelpers is reused across all rules in this loop.
|
|
250
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
251
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(ruleConfig && ruleConfig.excludeSelectors);
|
|
252
|
+
}
|
|
253
|
+
|
|
245
254
|
const ctx = {
|
|
246
255
|
document,
|
|
247
256
|
window,
|
|
@@ -312,6 +321,14 @@ function runCore(pageUrl, contextSelector, engineOptions, runOnly, CHECK_DEFS, R
|
|
|
312
321
|
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
313
322
|
}
|
|
314
323
|
|
|
324
|
+
// Composite rollups below carry no occurrences/nodes of their own, so
|
|
325
|
+
// they never exercise rule-scoped excludes -- but clear the "active
|
|
326
|
+
// rule" state on sharedHelpers regardless, so nothing after this point
|
|
327
|
+
// (composite aggregation, perf stats) can observe a stale rule's excludes.
|
|
328
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
329
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(null);
|
|
330
|
+
}
|
|
331
|
+
|
|
315
332
|
// =========================
|
|
316
333
|
// Composite rule aggregation (data-only rollups)
|
|
317
334
|
// =========================
|
package/src/core.js
CHANGED
|
@@ -10969,14 +10969,35 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
10969
10969
|
const includeShadowDom = !(opts && opts.includeShadowDom === false);
|
|
10970
10970
|
const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
|
|
10971
10971
|
|
|
10972
|
+
// Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
|
|
10973
|
+
// by dom-runner.js immediately before invoking each rule's applicability/
|
|
10974
|
+
// run function via __setActiveRuleExcludeSelectors(). Safe as mutable
|
|
10975
|
+
// closure state because rule execution is synchronous and single-rule-
|
|
10976
|
+
// at-a-time: exactly one rule's excludes are ever "active" at once.
|
|
10977
|
+
var __activeRuleExcludeSelectors = [];
|
|
10978
|
+
|
|
10979
|
+
function __getEffectiveExcludeSelectors() {
|
|
10980
|
+
return __activeRuleExcludeSelectors.length
|
|
10981
|
+
? excludeSelectors.concat(__activeRuleExcludeSelectors)
|
|
10982
|
+
: excludeSelectors;
|
|
10983
|
+
}
|
|
10984
|
+
|
|
10985
|
+
function __setActiveRuleExcludeSelectors(list) {
|
|
10986
|
+
__activeRuleExcludeSelectors = normalizeSelectorList(list);
|
|
10987
|
+
}
|
|
10988
|
+
|
|
10972
10989
|
// Selector-related caches (selector uniqueness index, per-element built
|
|
10973
|
-
// selector strings) depend on includeShadowDom/
|
|
10974
|
-
// those change which elements are considered when checking
|
|
10975
|
-
// The underlying storage is shared across createDomHelpers()
|
|
10976
|
-
// the same window/document (see __domSharedCache below), so a
|
|
10977
|
-
//
|
|
10978
|
-
//
|
|
10979
|
-
|
|
10990
|
+
// selector strings) depend on includeShadowDom/the effective exclude
|
|
10991
|
+
// list, since those change which elements are considered when checking
|
|
10992
|
+
// uniqueness. The underlying storage is shared across createDomHelpers()
|
|
10993
|
+
// calls on the same window/document (see __domSharedCache below), so a
|
|
10994
|
+
// run -- or a rule with its own rule-scoped excludes -- must not
|
|
10995
|
+
// read/write another run/rule's cached selectors. This key partitions
|
|
10996
|
+
// those caches per effective option set; recomputed per call (not a
|
|
10997
|
+
// constant) since the effective list changes as the active rule changes.
|
|
10998
|
+
function __getSelectorOptsKey() {
|
|
10999
|
+
return (includeShadowDom ? 'sd1' : 'sd0') + '|' + __getEffectiveExcludeSelectors().slice().sort().join(',');
|
|
11000
|
+
}
|
|
10980
11001
|
|
|
10981
11002
|
// -------------------------------------------------------------------------
|
|
10982
11003
|
// Per-run shared caches (DOM helpers)
|
|
@@ -11738,9 +11759,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
11738
11759
|
|
|
11739
11760
|
|
|
11740
11761
|
function isExcluded(el) {
|
|
11741
|
-
|
|
11762
|
+
const eff = __getEffectiveExcludeSelectors();
|
|
11763
|
+
if (!eff.length || !el || !el.closest) return false;
|
|
11742
11764
|
try {
|
|
11743
|
-
return
|
|
11765
|
+
return eff.some((sel) => !!el.closest(sel));
|
|
11744
11766
|
} catch {
|
|
11745
11767
|
return false;
|
|
11746
11768
|
}
|
|
@@ -11835,8 +11857,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
11835
11857
|
if (!scope || !scope.querySelectorAll) return [];
|
|
11836
11858
|
|
|
11837
11859
|
// Cache shadow root discovery per root to avoid repeated querySelectorAll('*') walks.
|
|
11838
|
-
// IMPORTANT: do not cache when
|
|
11839
|
-
|
|
11860
|
+
// IMPORTANT: do not cache when the effective exclude list (global
|
|
11861
|
+
// ∪ active rule-scoped excludes) is non-empty -- different rules
|
|
11862
|
+
// may have different effective lists and must not share results.
|
|
11863
|
+
if (!__getEffectiveExcludeSelectors().length && __shadowRootsByRoot) {
|
|
11840
11864
|
try {
|
|
11841
11865
|
const cached = __shadowRootsByRoot.get(scope);
|
|
11842
11866
|
if (cached) {
|
|
@@ -11911,7 +11935,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
11911
11935
|
|
|
11912
11936
|
function queryAllSmart(sel) {
|
|
11913
11937
|
const list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
|
|
11914
|
-
return
|
|
11938
|
+
return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
|
|
11915
11939
|
}
|
|
11916
11940
|
|
|
11917
11941
|
// -------------------------------------------------------------------------
|
|
@@ -11945,10 +11969,11 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
11945
11969
|
function __getSelectorCacheForOpts() {
|
|
11946
11970
|
if (!__selectorCache) return null;
|
|
11947
11971
|
try {
|
|
11948
|
-
|
|
11972
|
+
const key = __getSelectorOptsKey();
|
|
11973
|
+
let wm = __selectorCache.get(key);
|
|
11949
11974
|
if (!(wm instanceof WeakMap)) {
|
|
11950
11975
|
wm = new WeakMap();
|
|
11951
|
-
__selectorCache.set(
|
|
11976
|
+
__selectorCache.set(key, wm);
|
|
11952
11977
|
}
|
|
11953
11978
|
return wm;
|
|
11954
11979
|
} catch {
|
|
@@ -14359,7 +14384,8 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
14359
14384
|
return createSelectorUniqIndex();
|
|
14360
14385
|
}
|
|
14361
14386
|
|
|
14362
|
-
const
|
|
14387
|
+
const key = __getSelectorOptsKey();
|
|
14388
|
+
const cached = perScope.get(key);
|
|
14363
14389
|
if (cached) {
|
|
14364
14390
|
__perfInc('uniqIndex.hit');
|
|
14365
14391
|
return cached;
|
|
@@ -14368,7 +14394,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
14368
14394
|
__perfInc('uniqIndex.miss');
|
|
14369
14395
|
const idx = createSelectorUniqIndex();
|
|
14370
14396
|
try {
|
|
14371
|
-
perScope.set(
|
|
14397
|
+
perScope.set(key, idx);
|
|
14372
14398
|
} catch { /* ignore */
|
|
14373
14399
|
}
|
|
14374
14400
|
__perfInc('uniqIndex.build');
|
|
@@ -14878,6 +14904,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
14878
14904
|
isAccTreeEligible,
|
|
14879
14905
|
isDomVisibleEligible,
|
|
14880
14906
|
|
|
14907
|
+
// Engine-internal: sets which rule's rule-scoped excludeSelectors
|
|
14908
|
+
// (engineOptions.rules[ruleId].excludeSelectors) are currently in
|
|
14909
|
+
// effect. Called by dom-runner.js before each rule invocation, not
|
|
14910
|
+
// intended for use by rule implementations.
|
|
14911
|
+
__setActiveRuleExcludeSelectors,
|
|
14912
|
+
|
|
14881
14913
|
// Eligibility info wrapper
|
|
14882
14914
|
getEligibilityInfo,
|
|
14883
14915
|
|
|
@@ -15291,6 +15323,15 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
15291
15323
|
? engineOptionsResolved.rules[defResolved.ruleId]
|
|
15292
15324
|
: null;
|
|
15293
15325
|
|
|
15326
|
+
// Rule-scoped excludeSelectors (engineOptions.rules[ruleId].excludeSelectors)
|
|
15327
|
+
// apply on top of the global excludeSelectors for exactly this rule's
|
|
15328
|
+
// applicability check + run, then are cleared once this rule is done.
|
|
15329
|
+
// Safe because rule execution below is synchronous and one rule at a
|
|
15330
|
+
// time -- sharedHelpers is reused across all rules in this loop.
|
|
15331
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
15332
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(ruleConfig && ruleConfig.excludeSelectors);
|
|
15333
|
+
}
|
|
15334
|
+
|
|
15294
15335
|
const ctx = {
|
|
15295
15336
|
document,
|
|
15296
15337
|
window,
|
|
@@ -15361,6 +15402,14 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
15361
15402
|
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
15362
15403
|
}
|
|
15363
15404
|
|
|
15405
|
+
// Composite rollups below carry no occurrences/nodes of their own, so
|
|
15406
|
+
// they never exercise rule-scoped excludes -- but clear the "active
|
|
15407
|
+
// rule" state on sharedHelpers regardless, so nothing after this point
|
|
15408
|
+
// (composite aggregation, perf stats) can observe a stale rule's excludes.
|
|
15409
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
15410
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(null);
|
|
15411
|
+
}
|
|
15412
|
+
|
|
15364
15413
|
// =========================
|
|
15365
15414
|
// Composite rule aggregation (data-only rollups)
|
|
15366
15415
|
// =========================
|
|
@@ -43009,14 +43058,35 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
43009
43058
|
const includeShadowDom = !(opts && opts.includeShadowDom === false);
|
|
43010
43059
|
const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
|
|
43011
43060
|
|
|
43061
|
+
// Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
|
|
43062
|
+
// by dom-runner.js immediately before invoking each rule's applicability/
|
|
43063
|
+
// run function via __setActiveRuleExcludeSelectors(). Safe as mutable
|
|
43064
|
+
// closure state because rule execution is synchronous and single-rule-
|
|
43065
|
+
// at-a-time: exactly one rule's excludes are ever "active" at once.
|
|
43066
|
+
var __activeRuleExcludeSelectors = [];
|
|
43067
|
+
|
|
43068
|
+
function __getEffectiveExcludeSelectors() {
|
|
43069
|
+
return __activeRuleExcludeSelectors.length
|
|
43070
|
+
? excludeSelectors.concat(__activeRuleExcludeSelectors)
|
|
43071
|
+
: excludeSelectors;
|
|
43072
|
+
}
|
|
43073
|
+
|
|
43074
|
+
function __setActiveRuleExcludeSelectors(list) {
|
|
43075
|
+
__activeRuleExcludeSelectors = normalizeSelectorList(list);
|
|
43076
|
+
}
|
|
43077
|
+
|
|
43012
43078
|
// Selector-related caches (selector uniqueness index, per-element built
|
|
43013
|
-
// selector strings) depend on includeShadowDom/
|
|
43014
|
-
// those change which elements are considered when checking
|
|
43015
|
-
// The underlying storage is shared across createDomHelpers()
|
|
43016
|
-
// the same window/document (see __domSharedCache below), so a
|
|
43017
|
-
//
|
|
43018
|
-
//
|
|
43019
|
-
|
|
43079
|
+
// selector strings) depend on includeShadowDom/the effective exclude
|
|
43080
|
+
// list, since those change which elements are considered when checking
|
|
43081
|
+
// uniqueness. The underlying storage is shared across createDomHelpers()
|
|
43082
|
+
// calls on the same window/document (see __domSharedCache below), so a
|
|
43083
|
+
// run -- or a rule with its own rule-scoped excludes -- must not
|
|
43084
|
+
// read/write another run/rule's cached selectors. This key partitions
|
|
43085
|
+
// those caches per effective option set; recomputed per call (not a
|
|
43086
|
+
// constant) since the effective list changes as the active rule changes.
|
|
43087
|
+
function __getSelectorOptsKey() {
|
|
43088
|
+
return (includeShadowDom ? 'sd1' : 'sd0') + '|' + __getEffectiveExcludeSelectors().slice().sort().join(',');
|
|
43089
|
+
}
|
|
43020
43090
|
|
|
43021
43091
|
// -------------------------------------------------------------------------
|
|
43022
43092
|
// Per-run shared caches (DOM helpers)
|
|
@@ -43778,9 +43848,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
43778
43848
|
|
|
43779
43849
|
|
|
43780
43850
|
function isExcluded(el) {
|
|
43781
|
-
|
|
43851
|
+
const eff = __getEffectiveExcludeSelectors();
|
|
43852
|
+
if (!eff.length || !el || !el.closest) return false;
|
|
43782
43853
|
try {
|
|
43783
|
-
return
|
|
43854
|
+
return eff.some((sel) => !!el.closest(sel));
|
|
43784
43855
|
} catch {
|
|
43785
43856
|
return false;
|
|
43786
43857
|
}
|
|
@@ -43875,8 +43946,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
43875
43946
|
if (!scope || !scope.querySelectorAll) return [];
|
|
43876
43947
|
|
|
43877
43948
|
// Cache shadow root discovery per root to avoid repeated querySelectorAll('*') walks.
|
|
43878
|
-
// IMPORTANT: do not cache when
|
|
43879
|
-
|
|
43949
|
+
// IMPORTANT: do not cache when the effective exclude list (global
|
|
43950
|
+
// ∪ active rule-scoped excludes) is non-empty -- different rules
|
|
43951
|
+
// may have different effective lists and must not share results.
|
|
43952
|
+
if (!__getEffectiveExcludeSelectors().length && __shadowRootsByRoot) {
|
|
43880
43953
|
try {
|
|
43881
43954
|
const cached = __shadowRootsByRoot.get(scope);
|
|
43882
43955
|
if (cached) {
|
|
@@ -43951,7 +44024,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
43951
44024
|
|
|
43952
44025
|
function queryAllSmart(sel) {
|
|
43953
44026
|
const list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
|
|
43954
|
-
return
|
|
44027
|
+
return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
|
|
43955
44028
|
}
|
|
43956
44029
|
|
|
43957
44030
|
// -------------------------------------------------------------------------
|
|
@@ -43985,10 +44058,11 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
43985
44058
|
function __getSelectorCacheForOpts() {
|
|
43986
44059
|
if (!__selectorCache) return null;
|
|
43987
44060
|
try {
|
|
43988
|
-
|
|
44061
|
+
const key = __getSelectorOptsKey();
|
|
44062
|
+
let wm = __selectorCache.get(key);
|
|
43989
44063
|
if (!(wm instanceof WeakMap)) {
|
|
43990
44064
|
wm = new WeakMap();
|
|
43991
|
-
__selectorCache.set(
|
|
44065
|
+
__selectorCache.set(key, wm);
|
|
43992
44066
|
}
|
|
43993
44067
|
return wm;
|
|
43994
44068
|
} catch {
|
|
@@ -46399,7 +46473,8 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
46399
46473
|
return createSelectorUniqIndex();
|
|
46400
46474
|
}
|
|
46401
46475
|
|
|
46402
|
-
const
|
|
46476
|
+
const key = __getSelectorOptsKey();
|
|
46477
|
+
const cached = perScope.get(key);
|
|
46403
46478
|
if (cached) {
|
|
46404
46479
|
__perfInc('uniqIndex.hit');
|
|
46405
46480
|
return cached;
|
|
@@ -46408,7 +46483,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
46408
46483
|
__perfInc('uniqIndex.miss');
|
|
46409
46484
|
const idx = createSelectorUniqIndex();
|
|
46410
46485
|
try {
|
|
46411
|
-
perScope.set(
|
|
46486
|
+
perScope.set(key, idx);
|
|
46412
46487
|
} catch { /* ignore */
|
|
46413
46488
|
}
|
|
46414
46489
|
__perfInc('uniqIndex.build');
|
|
@@ -46918,6 +46993,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
46918
46993
|
isAccTreeEligible,
|
|
46919
46994
|
isDomVisibleEligible,
|
|
46920
46995
|
|
|
46996
|
+
// Engine-internal: sets which rule's rule-scoped excludeSelectors
|
|
46997
|
+
// (engineOptions.rules[ruleId].excludeSelectors) are currently in
|
|
46998
|
+
// effect. Called by dom-runner.js before each rule invocation, not
|
|
46999
|
+
// intended for use by rule implementations.
|
|
47000
|
+
__setActiveRuleExcludeSelectors,
|
|
47001
|
+
|
|
46921
47002
|
// Eligibility info wrapper
|
|
46922
47003
|
getEligibilityInfo,
|
|
46923
47004
|
|
|
@@ -47331,6 +47412,15 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
47331
47412
|
? engineOptionsResolved.rules[defResolved.ruleId]
|
|
47332
47413
|
: null;
|
|
47333
47414
|
|
|
47415
|
+
// Rule-scoped excludeSelectors (engineOptions.rules[ruleId].excludeSelectors)
|
|
47416
|
+
// apply on top of the global excludeSelectors for exactly this rule's
|
|
47417
|
+
// applicability check + run, then are cleared once this rule is done.
|
|
47418
|
+
// Safe because rule execution below is synchronous and one rule at a
|
|
47419
|
+
// time -- sharedHelpers is reused across all rules in this loop.
|
|
47420
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
47421
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(ruleConfig && ruleConfig.excludeSelectors);
|
|
47422
|
+
}
|
|
47423
|
+
|
|
47334
47424
|
const ctx = {
|
|
47335
47425
|
document,
|
|
47336
47426
|
window,
|
|
@@ -47401,6 +47491,14 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
47401
47491
|
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
47402
47492
|
}
|
|
47403
47493
|
|
|
47494
|
+
// Composite rollups below carry no occurrences/nodes of their own, so
|
|
47495
|
+
// they never exercise rule-scoped excludes -- but clear the "active
|
|
47496
|
+
// rule" state on sharedHelpers regardless, so nothing after this point
|
|
47497
|
+
// (composite aggregation, perf stats) can observe a stale rule's excludes.
|
|
47498
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
47499
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(null);
|
|
47500
|
+
}
|
|
47501
|
+
|
|
47404
47502
|
// =========================
|
|
47405
47503
|
// Composite rule aggregation (data-only rollups)
|
|
47406
47504
|
// =========================
|
|
@@ -75004,14 +75102,35 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
75004
75102
|
const includeShadowDom = !(opts && opts.includeShadowDom === false);
|
|
75005
75103
|
const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
|
|
75006
75104
|
|
|
75105
|
+
// Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
|
|
75106
|
+
// by dom-runner.js immediately before invoking each rule's applicability/
|
|
75107
|
+
// run function via __setActiveRuleExcludeSelectors(). Safe as mutable
|
|
75108
|
+
// closure state because rule execution is synchronous and single-rule-
|
|
75109
|
+
// at-a-time: exactly one rule's excludes are ever "active" at once.
|
|
75110
|
+
var __activeRuleExcludeSelectors = [];
|
|
75111
|
+
|
|
75112
|
+
function __getEffectiveExcludeSelectors() {
|
|
75113
|
+
return __activeRuleExcludeSelectors.length
|
|
75114
|
+
? excludeSelectors.concat(__activeRuleExcludeSelectors)
|
|
75115
|
+
: excludeSelectors;
|
|
75116
|
+
}
|
|
75117
|
+
|
|
75118
|
+
function __setActiveRuleExcludeSelectors(list) {
|
|
75119
|
+
__activeRuleExcludeSelectors = normalizeSelectorList(list);
|
|
75120
|
+
}
|
|
75121
|
+
|
|
75007
75122
|
// Selector-related caches (selector uniqueness index, per-element built
|
|
75008
|
-
// selector strings) depend on includeShadowDom/
|
|
75009
|
-
// those change which elements are considered when checking
|
|
75010
|
-
// The underlying storage is shared across createDomHelpers()
|
|
75011
|
-
// the same window/document (see __domSharedCache below), so a
|
|
75012
|
-
//
|
|
75013
|
-
//
|
|
75014
|
-
|
|
75123
|
+
// selector strings) depend on includeShadowDom/the effective exclude
|
|
75124
|
+
// list, since those change which elements are considered when checking
|
|
75125
|
+
// uniqueness. The underlying storage is shared across createDomHelpers()
|
|
75126
|
+
// calls on the same window/document (see __domSharedCache below), so a
|
|
75127
|
+
// run -- or a rule with its own rule-scoped excludes -- must not
|
|
75128
|
+
// read/write another run/rule's cached selectors. This key partitions
|
|
75129
|
+
// those caches per effective option set; recomputed per call (not a
|
|
75130
|
+
// constant) since the effective list changes as the active rule changes.
|
|
75131
|
+
function __getSelectorOptsKey() {
|
|
75132
|
+
return (includeShadowDom ? 'sd1' : 'sd0') + '|' + __getEffectiveExcludeSelectors().slice().sort().join(',');
|
|
75133
|
+
}
|
|
75015
75134
|
|
|
75016
75135
|
// -------------------------------------------------------------------------
|
|
75017
75136
|
// Per-run shared caches (DOM helpers)
|
|
@@ -75773,9 +75892,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
75773
75892
|
|
|
75774
75893
|
|
|
75775
75894
|
function isExcluded(el) {
|
|
75776
|
-
|
|
75895
|
+
const eff = __getEffectiveExcludeSelectors();
|
|
75896
|
+
if (!eff.length || !el || !el.closest) return false;
|
|
75777
75897
|
try {
|
|
75778
|
-
return
|
|
75898
|
+
return eff.some((sel) => !!el.closest(sel));
|
|
75779
75899
|
} catch {
|
|
75780
75900
|
return false;
|
|
75781
75901
|
}
|
|
@@ -75870,8 +75990,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
75870
75990
|
if (!scope || !scope.querySelectorAll) return [];
|
|
75871
75991
|
|
|
75872
75992
|
// Cache shadow root discovery per root to avoid repeated querySelectorAll('*') walks.
|
|
75873
|
-
// IMPORTANT: do not cache when
|
|
75874
|
-
|
|
75993
|
+
// IMPORTANT: do not cache when the effective exclude list (global
|
|
75994
|
+
// ∪ active rule-scoped excludes) is non-empty -- different rules
|
|
75995
|
+
// may have different effective lists and must not share results.
|
|
75996
|
+
if (!__getEffectiveExcludeSelectors().length && __shadowRootsByRoot) {
|
|
75875
75997
|
try {
|
|
75876
75998
|
const cached = __shadowRootsByRoot.get(scope);
|
|
75877
75999
|
if (cached) {
|
|
@@ -75946,7 +76068,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
75946
76068
|
|
|
75947
76069
|
function queryAllSmart(sel) {
|
|
75948
76070
|
const list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
|
|
75949
|
-
return
|
|
76071
|
+
return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
|
|
75950
76072
|
}
|
|
75951
76073
|
|
|
75952
76074
|
// -------------------------------------------------------------------------
|
|
@@ -75980,10 +76102,11 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
75980
76102
|
function __getSelectorCacheForOpts() {
|
|
75981
76103
|
if (!__selectorCache) return null;
|
|
75982
76104
|
try {
|
|
75983
|
-
|
|
76105
|
+
const key = __getSelectorOptsKey();
|
|
76106
|
+
let wm = __selectorCache.get(key);
|
|
75984
76107
|
if (!(wm instanceof WeakMap)) {
|
|
75985
76108
|
wm = new WeakMap();
|
|
75986
|
-
__selectorCache.set(
|
|
76109
|
+
__selectorCache.set(key, wm);
|
|
75987
76110
|
}
|
|
75988
76111
|
return wm;
|
|
75989
76112
|
} catch {
|
|
@@ -78394,7 +78517,8 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
78394
78517
|
return createSelectorUniqIndex();
|
|
78395
78518
|
}
|
|
78396
78519
|
|
|
78397
|
-
const
|
|
78520
|
+
const key = __getSelectorOptsKey();
|
|
78521
|
+
const cached = perScope.get(key);
|
|
78398
78522
|
if (cached) {
|
|
78399
78523
|
__perfInc('uniqIndex.hit');
|
|
78400
78524
|
return cached;
|
|
@@ -78403,7 +78527,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
78403
78527
|
__perfInc('uniqIndex.miss');
|
|
78404
78528
|
const idx = createSelectorUniqIndex();
|
|
78405
78529
|
try {
|
|
78406
|
-
perScope.set(
|
|
78530
|
+
perScope.set(key, idx);
|
|
78407
78531
|
} catch { /* ignore */
|
|
78408
78532
|
}
|
|
78409
78533
|
__perfInc('uniqIndex.build');
|
|
@@ -78913,6 +79037,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
78913
79037
|
isAccTreeEligible,
|
|
78914
79038
|
isDomVisibleEligible,
|
|
78915
79039
|
|
|
79040
|
+
// Engine-internal: sets which rule's rule-scoped excludeSelectors
|
|
79041
|
+
// (engineOptions.rules[ruleId].excludeSelectors) are currently in
|
|
79042
|
+
// effect. Called by dom-runner.js before each rule invocation, not
|
|
79043
|
+
// intended for use by rule implementations.
|
|
79044
|
+
__setActiveRuleExcludeSelectors,
|
|
79045
|
+
|
|
78916
79046
|
// Eligibility info wrapper
|
|
78917
79047
|
getEligibilityInfo,
|
|
78918
79048
|
|
|
@@ -79326,6 +79456,15 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
79326
79456
|
? engineOptionsResolved.rules[defResolved.ruleId]
|
|
79327
79457
|
: null;
|
|
79328
79458
|
|
|
79459
|
+
// Rule-scoped excludeSelectors (engineOptions.rules[ruleId].excludeSelectors)
|
|
79460
|
+
// apply on top of the global excludeSelectors for exactly this rule's
|
|
79461
|
+
// applicability check + run, then are cleared once this rule is done.
|
|
79462
|
+
// Safe because rule execution below is synchronous and one rule at a
|
|
79463
|
+
// time -- sharedHelpers is reused across all rules in this loop.
|
|
79464
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
79465
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(ruleConfig && ruleConfig.excludeSelectors);
|
|
79466
|
+
}
|
|
79467
|
+
|
|
79329
79468
|
const ctx = {
|
|
79330
79469
|
document,
|
|
79331
79470
|
window,
|
|
@@ -79396,6 +79535,14 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
79396
79535
|
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
79397
79536
|
}
|
|
79398
79537
|
|
|
79538
|
+
// Composite rollups below carry no occurrences/nodes of their own, so
|
|
79539
|
+
// they never exercise rule-scoped excludes -- but clear the "active
|
|
79540
|
+
// rule" state on sharedHelpers regardless, so nothing after this point
|
|
79541
|
+
// (composite aggregation, perf stats) can observe a stale rule's excludes.
|
|
79542
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
79543
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(null);
|
|
79544
|
+
}
|
|
79545
|
+
|
|
79399
79546
|
// =========================
|
|
79400
79547
|
// Composite rule aggregation (data-only rollups)
|
|
79401
79548
|
// =========================
|