@surea11y/core 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -2
- package/docs/ENGINE_OPTIONS.md +33 -3
- package/docs/LIMITATIONS.md +1 -1
- package/docs/TROUBLESHOOTING.md +1 -1
- package/package.json +2 -1
- package/src/checks/automatic/aria-allowed-attr.js +3 -1
- package/src/checks/automatic/aria-allowed-role.js +3 -1
- package/src/checks/automatic/aria-prohibited-attr.js +3 -1
- package/src/checks/automatic/aria-valid-attr-value.js +3 -1
- package/src/checks/automatic/aria-valid-attr.js +3 -1
- package/src/checks/automatic/deprecated-elements-not-used.js +3 -1
- package/src/checks/automatic/iframe-title-unique.js +3 -2
- package/src/checks/automatic/table-headers-attr-valid.js +3 -1
- package/src/checks/automatic/table-th-has-data-cells.js +3 -1
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +4 -2
- package/src/core/dom-helpers.js +82 -17
- package/src/core/dom-runner.js +21 -0
- package/src/core.js +309 -51
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,18 @@ All notable changes to this project are documented here, in [Keep a Changelog](h
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.1.1] - 2026-07-29
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **`engineOptions.includeHiddenElements` (default `false`)**: helper-driven rules now skip elements hidden by `display:none` (on the element or any ancestor), `visibility:hidden`/`collapse`, the `[hidden]` attribute, closed `<details>`, and other structurally-non-rendered content by default, matching the visibility-aware behavior of other established engines. Filtering happens upstream in the shared `queryAllSmart` helper, before a rule's own pass/fail logic runs, so it's a candidate-list exclusion, not a post-hoc annotation. Set `engineOptions.includeHiddenElements: true` to restore the previous behavior and evaluate hidden/collapsed subtrees anyway (e.g. to catch a markup defect, like a broken ARIA ID reference, before a `<dialog>` ever opens). 10 rule files whose own logic intentionally doesn't call the underlying eligibility check directly (static-markup-validity rules such as `aria-valid-attr`, `aria-valid-attr-value`, `aria-allowed-attr`, `aria-allowed-role`, `aria-prohibited-attr`, `table-headers-attr-valid`, `table-th-has-data-cells`, `deprecated-elements-not-used`, `iframe-title-unique`, `aria-checked-state-mismatch-manual`) still inherit this filtering through `queryAllSmart`; their doc comments were updated to say so. See `docs/ENGINE_OPTIONS.md` and `docs/LIMITATIONS.md`.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `docs/LIMITATIONS.md`: the `<dialog>`/UA-stylesheet-hidden-content note was stale — it claimed static-markup-validity checks still evaluate hidden content, which this release's default change makes no longer true. Corrected to describe the current default and how to opt back in.
|
|
14
|
+
|
|
15
|
+
## [1.1.0] - 2026-07-28
|
|
16
|
+
|
|
7
17
|
### Added
|
|
18
|
+
- `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
19
|
- 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
20
|
|
|
10
21
|
### Fixed
|
|
@@ -25,7 +36,7 @@ All notable changes to this project are documented here, in [Keep a Changelog](h
|
|
|
25
36
|
- README: the JSON output example referenced a nonexistent rule id (`link-name-quality`); corrected to the real id, `link-name-quality-manual`.
|
|
26
37
|
- README: Quick Start code samples labeled the runner's four positional arguments as `url, ruleFilter, options, policy`; corrected to the actual names (`pageUrl, contextSelector, engineOptions, runOnly`) used consistently elsewhere in the docs.
|
|
27
38
|
|
|
28
|
-
## [1.
|
|
39
|
+
## [1.0.0] - 2026-07-26
|
|
29
40
|
|
|
30
41
|
### Added
|
|
31
42
|
- 125 rules (77 automatic/`fail`-capable, 48 manual/advisory) — see `docs/RULE_CATALOG.md` for the full list.
|
|
@@ -58,7 +69,7 @@ See `docs/LIMITATIONS.md` — structural (keyboard-trap detection, reflow-at-zoo
|
|
|
58
69
|
|
|
59
70
|
---
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
# How to add an entry
|
|
62
73
|
|
|
63
74
|
When you ship a change worth calling out to consumers (not every commit):
|
|
64
75
|
1. Add a bullet under `[Unreleased]`, in the right subsection (`Added`, `Changed`, `Fixed`, `Deprecated`, `Removed`, `Security`) — create the subsection if it doesn't exist yet for this cycle.
|
package/docs/ENGINE_OPTIONS.md
CHANGED
|
@@ -72,6 +72,7 @@ runDomRulesInPage(url, null, {
|
|
|
72
72
|
```js
|
|
73
73
|
const engineOptions = {
|
|
74
74
|
locale: 'en', // default 'en'; falls back to 'en' per-string if a key is missing in the requested locale
|
|
75
|
+
includeHiddenElements: false, // default false — set true to evaluate hidden/collapsed subtrees too
|
|
75
76
|
includeShadowDom: true, // default true — opt OUT with `false` to skip open shadow roots
|
|
76
77
|
excludeSelectors: ['#cookie-banner', '.third-party-widget'], // array or comma-separated string
|
|
77
78
|
timestamp: '2026-07-20T12:00:00Z', // optional — engine has no built-in clock, see OUTPUT_SCHEMA.md
|
|
@@ -95,7 +96,9 @@ const engineOptions = {
|
|
|
95
96
|
},
|
|
96
97
|
|
|
97
98
|
rules: {
|
|
98
|
-
'some-rule-id': {
|
|
99
|
+
'some-rule-id': {
|
|
100
|
+
excludeSelectors: ['.some-noisy-widget'] // narrows candidates for THIS rule only — see note
|
|
101
|
+
}
|
|
99
102
|
},
|
|
100
103
|
|
|
101
104
|
probes: { /* optional host-supplied evidence, see note */ },
|
|
@@ -112,19 +115,46 @@ const engineOptions = {
|
|
|
112
115
|
| Option | Meaning |
|
|
113
116
|
|---|---|
|
|
114
117
|
| `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. |
|
|
118
|
+
| `includeHiddenElements` | Default `false`: helper queries exclude elements hidden by structural/CSS mechanisms such as `display:none`, `[hidden]`, closed `<details>`, and hidden rendering-only host elements (with descendants excluded too). Set `true` to include those hidden/collapsed subtrees in evaluation (legacy/static-markup behavior). |
|
|
115
119
|
| `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. |
|
|
120
|
+
| `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
121
|
| `timestamp` | Passed straight through to the result's top-level `timestamp` field; the engine does not generate one itself (deterministic-by-design). |
|
|
118
122
|
| `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
123
|
| `contrast.rootCanvasFallback` | The assumed page background color when it's not computable at all — only matters in `auditorAssist` mode. |
|
|
120
124
|
| `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
125
|
| `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
126
|
| `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
|
|
127
|
+
| `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
128
|
| `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
129
|
| `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
130
|
| `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
131
|
|
|
132
|
+
### Rule-scoped `excludeSelectors`
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const engineOptions = {
|
|
138
|
+
excludeSelectors: ['#cookie-banner'], // applies to every rule, as always
|
|
139
|
+
rules: {
|
|
140
|
+
'aria-required-children': {
|
|
141
|
+
excludeSelectors: ['mat-select', 'mat-stepper', 'mat-horizontal-stepper', 'mat-vertical-stepper']
|
|
142
|
+
},
|
|
143
|
+
'aria-allowed-attr': {
|
|
144
|
+
excludeSelectors: ['mat-progress-spinner']
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
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.
|
|
151
|
+
|
|
152
|
+
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)).
|
|
153
|
+
|
|
154
|
+
Accepts the same forms as the global option: an array (`['mat-select', 'mat-stepper']`) or a comma-separated string (`'mat-select, mat-stepper'`).
|
|
155
|
+
|
|
156
|
+
> 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.
|
|
157
|
+
|
|
128
158
|
## Recipes — composing options for real scenarios
|
|
129
159
|
|
|
130
160
|
The reference above documents each option in isolation. These combine several at once, for scenarios you're likely to actually hit.
|
package/docs/LIMITATIONS.md
CHANGED
|
@@ -13,7 +13,7 @@ surea11y is a **static DOM scan**: it reads the DOM tree and computed styles at
|
|
|
13
13
|
## Environment-dependent — depends on how you run it
|
|
14
14
|
|
|
15
15
|
- **jsdom (Node, no real browser) has no CSS layout engine.** Rules needing real geometry — most notably `target-size-minimum` (WCAG 2.5.8, needs real `getBoundingClientRect()`) — report `notApplicable` under plain jsdom rather than guess. Run under a real browser (Puppeteer/Playwright — see [`INTEGRATION.md`](./INTEGRATION.md) Pattern 2) to get real findings from these rules.
|
|
16
|
-
- **`<dialog>` and other elements hidden by the default UA stylesheet** (no `open` attribute, `display: none` by spec) are
|
|
16
|
+
- **`<dialog>` and other elements hidden by the default UA stylesheet** (no `open` attribute, `display: none` by spec), along with any other subtree hidden via `display:none`, `visibility:hidden`, `[hidden]`, or closed `<details>`, are **excluded from rule evaluation by default** — matching the visibility-aware behavior of other established engines. This is a deliberate default (`engineOptions.includeHiddenElements: false`), not an oversight: hidden content isn't reachable by assistive technology or keyboard until it's shown, so flagging a markup defect inside it by default would often be noise. Set `engineOptions.includeHiddenElements: true` to evaluate hidden/collapsed subtrees anyway — e.g. to catch a markup defect (like a broken ARIA ID reference) before a dialog ever opens. See [`ENGINE_OPTIONS.md`](./ENGINE_OPTIONS.md#engineoptions--the-rest) for the option and exactly which hiding mechanisms it covers.
|
|
17
17
|
- **Static markup vs. live/post-hydration DOM state.** The rule logic itself is DOM-source-agnostic — it evaluates whatever DOM it's handed, whether that's jsdom-parsed static HTML (Pattern 1) or an already-loaded, already-hydrated real browser tab (Pattern 2, see [`INTEGRATION.md`](./INTEGRATION.md)). But the CLI (`npx @surea11y/core scan <url>`) specifically fetches static HTML only, with no JS execution — see [`CLI.md`](./CLI.md). For a JS-framework-hydrated widget whose server-rendered markup intentionally ships one state before client JS syncs it (e.g. `<input type="checkbox" aria-checked="true">` shipped before client JS sets the native `checked` property to match on hydration — an extremely common, entirely legitimate pattern), a CLI scan only sees the pre-hydration markup. Other engines running inside an actual loaded browser tab see the post-hydration state instead, so the two can disagree on exactly this class of element for reasons that have nothing to do with either engine's rule correctness. `aria-checked-state-mismatch` is deliberately `manual`/`cantTell`-capped for this exact reason rather than a hard `fail`. If you need live-DOM accuracy for hydration-sensitive checks, run the library directly against an already-loaded page via Pattern 2, not the static-fetch CLI.
|
|
18
18
|
|
|
19
19
|
## Deliberately not attempted — judgment calls, not automatable safely
|
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.
|
|
3
|
+
"version": "1.1.1",
|
|
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",
|
|
@@ -70,7 +70,9 @@
|
|
|
70
70
|
* menuitemcheckbox, menuitemradio. `aria-level` added to: tablist.
|
|
71
71
|
* - `tree`'s `aria-readonly` removed: not in aria-query's resolved
|
|
72
72
|
* props for `tree` (was an unverified carryover, not spec-backed).
|
|
73
|
-
* - Not gated on isAccTreeEligible: this
|
|
73
|
+
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
74
|
+
* property, while engine-level hidden-subtree filtering still applies
|
|
75
|
+
* unless engineOptions.includeHiddenElements is true.
|
|
74
76
|
*/
|
|
75
77
|
|
|
76
78
|
const id = 'aria-allowed-attr';
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* - Deliberately scoped to elements present in ALLOWED_ROLES_BY_ELEMENT;
|
|
19
19
|
* elements without an asserted constraint are treated as "no constraint"
|
|
20
20
|
* (not flagged) rather than guessed at — see that table's header comment.
|
|
21
|
-
* - Not gated on isAccTreeEligible: this
|
|
21
|
+
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
22
|
+
* property, while engine-level hidden-subtree filtering still applies
|
|
23
|
+
* unless engineOptions.includeHiddenElements is true.
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
const id = 'aria-allowed-role';
|
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
* `definition_role` page even demonstrates `aria-labelledby` usage on
|
|
56
56
|
* it directly. A real, confirmed documentation bug on MDN's side, not
|
|
57
57
|
* a gap here.
|
|
58
|
-
* - Not gated on isAccTreeEligible: this
|
|
58
|
+
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
59
|
+
* property, while engine-level hidden-subtree filtering still applies
|
|
60
|
+
* unless engineOptions.includeHiddenElements is true.
|
|
59
61
|
*/
|
|
60
62
|
|
|
61
63
|
const id = 'aria-prohibited-attr';
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* ID reference (list) that resolves to an existing element in the
|
|
18
18
|
* document.
|
|
19
19
|
* @implementation-notes
|
|
20
|
-
* - Not gated on isAccTreeEligible: this
|
|
20
|
+
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
21
|
+
* property, while engine-level hidden-subtree filtering still applies
|
|
22
|
+
* unless engineOptions.includeHiddenElements is true.
|
|
21
23
|
* - ID-reference resolution (added 2026-07-20, see aria-helpers.js's
|
|
22
24
|
* idExists) only flags idref-list attributes (aria-labelledby,
|
|
23
25
|
* aria-describedby, aria-controls, aria-owns, etc.) when NONE of the
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* - Distinct from aria-valid-attr-value (which validates the VALUE
|
|
19
19
|
* of a recognized attribute) — this rule only validates the attribute
|
|
20
20
|
* NAME.
|
|
21
|
-
* - Not gated on isAccTreeEligible: this
|
|
21
|
+
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
22
|
+
* property, while engine-level hidden-subtree filtering still applies
|
|
23
|
+
* unless engineOptions.includeHiddenElements is true.
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
const id = 'aria-valid-attr';
|
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
* rule has no partial-pass case, matching a widely-used reference engine's `blink`/`marquee`
|
|
18
18
|
* rules (which report only when the element is found).
|
|
19
19
|
* @implementation-notes
|
|
20
|
-
* - Not gated on isAccTreeEligible: presence in markup is itself the
|
|
20
|
+
* - Not rule-gated on isAccTreeEligible: presence in markup is itself the
|
|
21
21
|
* violation, independent of visibility (moving/blinking content inside a
|
|
22
22
|
* hidden ancestor could still become visible later without a code
|
|
23
23
|
* change, so hiding it today does not remove the underlying defect).
|
|
24
|
+
* Engine-level hidden-subtree filtering still applies unless
|
|
25
|
+
* engineOptions.includeHiddenElements is true.
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
28
|
const id = 'deprecated-elements-not-used';
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
* - Compares the title ATTRIBUTE specifically, not the full computed
|
|
20
20
|
* accessible name (aria-label could legitimately differ in wording even
|
|
21
21
|
* when title happens to collide) — matches a widely-used reference engine's frame-title-unique.
|
|
22
|
-
* - Not gated on isAccTreeEligible:
|
|
23
|
-
* property
|
|
22
|
+
* - Not rule-gated on isAccTreeEligible: duplicate titles are a static
|
|
23
|
+
* markup property. Engine-level hidden-subtree filtering still applies
|
|
24
|
+
* unless engineOptions.includeHiddenElements is true.
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
27
|
const id = 'iframe-title-unique';
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* - One occurrence per offending cell (not per bad token), listing every
|
|
17
17
|
* invalid reference — matches a widely-used reference engine's
|
|
18
18
|
* td-headers-attr reporting granularity.
|
|
19
|
-
* - Not gated on isAccTreeEligible: this
|
|
19
|
+
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
20
|
+
* property, while engine-level hidden-subtree filtering still applies
|
|
21
|
+
* unless engineOptions.includeHiddenElements is true.
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
const id = 'table-headers-attr-valid';
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
* if some particular <th> in it doesn't actually describe any cell
|
|
29
29
|
* (false negative, not a false positive — acceptable under this
|
|
30
30
|
* engine's philosophy).
|
|
31
|
-
* - Not gated on isAccTreeEligible: this
|
|
31
|
+
* - Not rule-gated on isAccTreeEligible: this remains a static-markup
|
|
32
|
+
* property, while engine-level hidden-subtree filtering still applies
|
|
33
|
+
* unless engineOptions.includeHiddenElements is true.
|
|
32
34
|
*/
|
|
33
35
|
|
|
34
36
|
const id = 'table-th-has-data-cells';
|
|
@@ -42,8 +42,10 @@
|
|
|
42
42
|
* - Any `aria-checked` value other than "true"/"false"/"mixed" (checkbox)
|
|
43
43
|
* or "true"/"false" (radio) is treated as equivalent to "false" — also
|
|
44
44
|
* matches that reference engine's own normalization exactly.
|
|
45
|
-
* - Not gated on isAccTreeEligible: whether the accessible state
|
|
46
|
-
* is a markup-correctness property
|
|
45
|
+
* - Not rule-gated on isAccTreeEligible: whether the accessible state
|
|
46
|
+
* matches is still a markup-correctness property, while engine-level
|
|
47
|
+
* hidden-subtree filtering applies unless engineOptions.includeHiddenElements
|
|
48
|
+
* is true.
|
|
47
49
|
*/
|
|
48
50
|
|
|
49
51
|
const id = 'aria-checked-state-mismatch';
|
package/src/core/dom-helpers.js
CHANGED
|
@@ -115,16 +115,41 @@ function createDomHelpers(opts) {
|
|
|
115
115
|
})();
|
|
116
116
|
// Default on: opt OUT with `includeShadowDom: false`, not opt in.
|
|
117
117
|
const includeShadowDom = !(opts && opts.includeShadowDom === false);
|
|
118
|
+
// Default off: by default, helper queries skip structurally/CSS-hidden
|
|
119
|
+
// subtrees (display:none, [hidden], closed <details>, etc.). Callers can
|
|
120
|
+
// opt out with includeHiddenElements:true.
|
|
121
|
+
const includeHiddenElements = !!(opts && opts.includeHiddenElements === true);
|
|
118
122
|
const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
|
|
119
123
|
|
|
124
|
+
// Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
|
|
125
|
+
// by dom-runner.js immediately before invoking each rule's applicability/
|
|
126
|
+
// run function via __setActiveRuleExcludeSelectors(). Safe as mutable
|
|
127
|
+
// closure state because rule execution is synchronous and single-rule-
|
|
128
|
+
// at-a-time: exactly one rule's excludes are ever "active" at once.
|
|
129
|
+
var __activeRuleExcludeSelectors = [];
|
|
130
|
+
|
|
131
|
+
function __getEffectiveExcludeSelectors() {
|
|
132
|
+
return __activeRuleExcludeSelectors.length
|
|
133
|
+
? excludeSelectors.concat(__activeRuleExcludeSelectors)
|
|
134
|
+
: excludeSelectors;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function __setActiveRuleExcludeSelectors(list) {
|
|
138
|
+
__activeRuleExcludeSelectors = normalizeSelectorList(list);
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
// 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
|
-
|
|
142
|
+
// selector strings) depend on includeShadowDom/the effective exclude
|
|
143
|
+
// list, since those change which elements are considered when checking
|
|
144
|
+
// uniqueness. The underlying storage is shared across createDomHelpers()
|
|
145
|
+
// calls on the same window/document (see __domSharedCache below), so a
|
|
146
|
+
// run -- or a rule with its own rule-scoped excludes -- must not
|
|
147
|
+
// read/write another run/rule's cached selectors. This key partitions
|
|
148
|
+
// those caches per effective option set; recomputed per call (not a
|
|
149
|
+
// constant) since the effective list changes as the active rule changes.
|
|
150
|
+
function __getSelectorOptsKey() {
|
|
151
|
+
return (includeShadowDom ? 'sd1' : 'sd0') + '|' + __getEffectiveExcludeSelectors().slice().sort().join(',');
|
|
152
|
+
}
|
|
128
153
|
|
|
129
154
|
// -------------------------------------------------------------------------
|
|
130
155
|
// Per-run shared caches (DOM helpers)
|
|
@@ -886,9 +911,10 @@ function createDomHelpers(opts) {
|
|
|
886
911
|
|
|
887
912
|
|
|
888
913
|
function isExcluded(el) {
|
|
889
|
-
|
|
914
|
+
const eff = __getEffectiveExcludeSelectors();
|
|
915
|
+
if (!eff.length || !el || !el.closest) return false;
|
|
890
916
|
try {
|
|
891
|
-
return
|
|
917
|
+
return eff.some((sel) => !!el.closest(sel));
|
|
892
918
|
} catch {
|
|
893
919
|
return false;
|
|
894
920
|
}
|
|
@@ -983,8 +1009,10 @@ function createDomHelpers(opts) {
|
|
|
983
1009
|
if (!scope || !scope.querySelectorAll) return [];
|
|
984
1010
|
|
|
985
1011
|
// Cache shadow root discovery per root to avoid repeated querySelectorAll('*') walks.
|
|
986
|
-
// IMPORTANT: do not cache when
|
|
987
|
-
|
|
1012
|
+
// IMPORTANT: do not cache when the effective exclude list (global
|
|
1013
|
+
// ∪ active rule-scoped excludes) is non-empty -- different rules
|
|
1014
|
+
// may have different effective lists and must not share results.
|
|
1015
|
+
if (!__getEffectiveExcludeSelectors().length && __shadowRootsByRoot) {
|
|
988
1016
|
try {
|
|
989
1017
|
const cached = __shadowRootsByRoot.get(scope);
|
|
990
1018
|
if (cached) {
|
|
@@ -1057,9 +1085,38 @@ function createDomHelpers(opts) {
|
|
|
1057
1085
|
return results;
|
|
1058
1086
|
}
|
|
1059
1087
|
|
|
1088
|
+
const HARD_HIDDEN_REASONS = new Set([
|
|
1089
|
+
'displayNone',
|
|
1090
|
+
'hiddenAttr',
|
|
1091
|
+
'detailsClosed',
|
|
1092
|
+
'templateContent',
|
|
1093
|
+
'nonRenderedElement',
|
|
1094
|
+
'inputHidden',
|
|
1095
|
+
'visibilityHidden'
|
|
1096
|
+
]);
|
|
1097
|
+
|
|
1060
1098
|
function queryAllSmart(sel) {
|
|
1061
|
-
|
|
1062
|
-
|
|
1099
|
+
let list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
|
|
1100
|
+
|
|
1101
|
+
// Global hidden-content policy: skip nodes that are fully excluded from
|
|
1102
|
+
// rendered visibility by default (unless includeHiddenElements:true).
|
|
1103
|
+
if (!includeHiddenElements) {
|
|
1104
|
+
list = list.filter((el) => {
|
|
1105
|
+
try {
|
|
1106
|
+
const vis = isAccTreeEligible(el);
|
|
1107
|
+
if (!vis || vis.eligible !== false) return true;
|
|
1108
|
+
const reasons = Array.isArray(vis.reasons) ? vis.reasons : [];
|
|
1109
|
+
for (const r of reasons) {
|
|
1110
|
+
if (HARD_HIDDEN_REASONS.has(r)) return false;
|
|
1111
|
+
}
|
|
1112
|
+
return true;
|
|
1113
|
+
} catch {
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
|
|
1063
1120
|
}
|
|
1064
1121
|
|
|
1065
1122
|
// -------------------------------------------------------------------------
|
|
@@ -1093,10 +1150,11 @@ function createDomHelpers(opts) {
|
|
|
1093
1150
|
function __getSelectorCacheForOpts() {
|
|
1094
1151
|
if (!__selectorCache) return null;
|
|
1095
1152
|
try {
|
|
1096
|
-
|
|
1153
|
+
const key = __getSelectorOptsKey();
|
|
1154
|
+
let wm = __selectorCache.get(key);
|
|
1097
1155
|
if (!(wm instanceof WeakMap)) {
|
|
1098
1156
|
wm = new WeakMap();
|
|
1099
|
-
__selectorCache.set(
|
|
1157
|
+
__selectorCache.set(key, wm);
|
|
1100
1158
|
}
|
|
1101
1159
|
return wm;
|
|
1102
1160
|
} catch {
|
|
@@ -3507,7 +3565,8 @@ function createDomHelpers(opts) {
|
|
|
3507
3565
|
return createSelectorUniqIndex();
|
|
3508
3566
|
}
|
|
3509
3567
|
|
|
3510
|
-
const
|
|
3568
|
+
const key = __getSelectorOptsKey();
|
|
3569
|
+
const cached = perScope.get(key);
|
|
3511
3570
|
if (cached) {
|
|
3512
3571
|
__perfInc('uniqIndex.hit');
|
|
3513
3572
|
return cached;
|
|
@@ -3516,7 +3575,7 @@ function createDomHelpers(opts) {
|
|
|
3516
3575
|
__perfInc('uniqIndex.miss');
|
|
3517
3576
|
const idx = createSelectorUniqIndex();
|
|
3518
3577
|
try {
|
|
3519
|
-
perScope.set(
|
|
3578
|
+
perScope.set(key, idx);
|
|
3520
3579
|
} catch { /* ignore */
|
|
3521
3580
|
}
|
|
3522
3581
|
__perfInc('uniqIndex.build');
|
|
@@ -4026,6 +4085,12 @@ function createDomHelpers(opts) {
|
|
|
4026
4085
|
isAccTreeEligible,
|
|
4027
4086
|
isDomVisibleEligible,
|
|
4028
4087
|
|
|
4088
|
+
// Engine-internal: sets which rule's rule-scoped excludeSelectors
|
|
4089
|
+
// (engineOptions.rules[ruleId].excludeSelectors) are currently in
|
|
4090
|
+
// effect. Called by dom-runner.js before each rule invocation, not
|
|
4091
|
+
// intended for use by rule implementations.
|
|
4092
|
+
__setActiveRuleExcludeSelectors,
|
|
4093
|
+
|
|
4029
4094
|
// Eligibility info wrapper
|
|
4030
4095
|
getEligibilityInfo,
|
|
4031
4096
|
|
package/src/core/dom-runner.js
CHANGED
|
@@ -48,6 +48,9 @@ function runCore(pageUrl, contextSelector, engineOptions, runOnly, CHECK_DEFS, R
|
|
|
48
48
|
|
|
49
49
|
// Default on: opt OUT with `includeShadowDom: false`, not opt in.
|
|
50
50
|
const includeShadowDom = !(engineOptionsResolved && engineOptionsResolved.includeShadowDom === false);
|
|
51
|
+
// Default off: hidden/collapsed content is excluded from rule evaluation
|
|
52
|
+
// unless the caller explicitly opts in.
|
|
53
|
+
const includeHiddenElements = !!(engineOptionsResolved && engineOptionsResolved.includeHiddenElements === true);
|
|
51
54
|
const excludeSelectors = normalizeSelectorList(engineOptionsResolved && engineOptionsResolved.excludeSelectors);
|
|
52
55
|
|
|
53
56
|
const url = pageUrl || (document.location && document.location.href) || null;
|
|
@@ -63,6 +66,7 @@ function runCore(pageUrl, contextSelector, engineOptions, runOnly, CHECK_DEFS, R
|
|
|
63
66
|
window,
|
|
64
67
|
root: roots,
|
|
65
68
|
includeShadowDom,
|
|
69
|
+
includeHiddenElements,
|
|
66
70
|
excludeSelectors,
|
|
67
71
|
// Optional perf counters (bench/debug only). Deterministic and per-run.
|
|
68
72
|
perfStats: !!(engineOptionsResolved && engineOptionsResolved.perfStats)
|
|
@@ -242,6 +246,15 @@ function runCore(pageUrl, contextSelector, engineOptions, runOnly, CHECK_DEFS, R
|
|
|
242
246
|
? engineOptionsResolved.rules[defResolved.ruleId]
|
|
243
247
|
: null;
|
|
244
248
|
|
|
249
|
+
// Rule-scoped excludeSelectors (engineOptions.rules[ruleId].excludeSelectors)
|
|
250
|
+
// apply on top of the global excludeSelectors for exactly this rule's
|
|
251
|
+
// applicability check + run, then are cleared once this rule is done.
|
|
252
|
+
// Safe because rule execution below is synchronous and one rule at a
|
|
253
|
+
// time -- sharedHelpers is reused across all rules in this loop.
|
|
254
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
255
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(ruleConfig && ruleConfig.excludeSelectors);
|
|
256
|
+
}
|
|
257
|
+
|
|
245
258
|
const ctx = {
|
|
246
259
|
document,
|
|
247
260
|
window,
|
|
@@ -312,6 +325,14 @@ function runCore(pageUrl, contextSelector, engineOptions, runOnly, CHECK_DEFS, R
|
|
|
312
325
|
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
313
326
|
}
|
|
314
327
|
|
|
328
|
+
// Composite rollups below carry no occurrences/nodes of their own, so
|
|
329
|
+
// they never exercise rule-scoped excludes -- but clear the "active
|
|
330
|
+
// rule" state on sharedHelpers regardless, so nothing after this point
|
|
331
|
+
// (composite aggregation, perf stats) can observe a stale rule's excludes.
|
|
332
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
333
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(null);
|
|
334
|
+
}
|
|
335
|
+
|
|
315
336
|
// =========================
|
|
316
337
|
// Composite rule aggregation (data-only rollups)
|
|
317
338
|
// =========================
|
package/src/core.js
CHANGED
|
@@ -10967,16 +10967,41 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
10967
10967
|
})();
|
|
10968
10968
|
// Default on: opt OUT with `includeShadowDom: false`, not opt in.
|
|
10969
10969
|
const includeShadowDom = !(opts && opts.includeShadowDom === false);
|
|
10970
|
+
// Default off: by default, helper queries skip structurally/CSS-hidden
|
|
10971
|
+
// subtrees (display:none, [hidden], closed <details>, etc.). Callers can
|
|
10972
|
+
// opt out with includeHiddenElements:true.
|
|
10973
|
+
const includeHiddenElements = !!(opts && opts.includeHiddenElements === true);
|
|
10970
10974
|
const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
|
|
10971
10975
|
|
|
10976
|
+
// Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
|
|
10977
|
+
// by dom-runner.js immediately before invoking each rule's applicability/
|
|
10978
|
+
// run function via __setActiveRuleExcludeSelectors(). Safe as mutable
|
|
10979
|
+
// closure state because rule execution is synchronous and single-rule-
|
|
10980
|
+
// at-a-time: exactly one rule's excludes are ever "active" at once.
|
|
10981
|
+
var __activeRuleExcludeSelectors = [];
|
|
10982
|
+
|
|
10983
|
+
function __getEffectiveExcludeSelectors() {
|
|
10984
|
+
return __activeRuleExcludeSelectors.length
|
|
10985
|
+
? excludeSelectors.concat(__activeRuleExcludeSelectors)
|
|
10986
|
+
: excludeSelectors;
|
|
10987
|
+
}
|
|
10988
|
+
|
|
10989
|
+
function __setActiveRuleExcludeSelectors(list) {
|
|
10990
|
+
__activeRuleExcludeSelectors = normalizeSelectorList(list);
|
|
10991
|
+
}
|
|
10992
|
+
|
|
10972
10993
|
// 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
|
-
|
|
10994
|
+
// selector strings) depend on includeShadowDom/the effective exclude
|
|
10995
|
+
// list, since those change which elements are considered when checking
|
|
10996
|
+
// uniqueness. The underlying storage is shared across createDomHelpers()
|
|
10997
|
+
// calls on the same window/document (see __domSharedCache below), so a
|
|
10998
|
+
// run -- or a rule with its own rule-scoped excludes -- must not
|
|
10999
|
+
// read/write another run/rule's cached selectors. This key partitions
|
|
11000
|
+
// those caches per effective option set; recomputed per call (not a
|
|
11001
|
+
// constant) since the effective list changes as the active rule changes.
|
|
11002
|
+
function __getSelectorOptsKey() {
|
|
11003
|
+
return (includeShadowDom ? 'sd1' : 'sd0') + '|' + __getEffectiveExcludeSelectors().slice().sort().join(',');
|
|
11004
|
+
}
|
|
10980
11005
|
|
|
10981
11006
|
// -------------------------------------------------------------------------
|
|
10982
11007
|
// Per-run shared caches (DOM helpers)
|
|
@@ -11738,9 +11763,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
11738
11763
|
|
|
11739
11764
|
|
|
11740
11765
|
function isExcluded(el) {
|
|
11741
|
-
|
|
11766
|
+
const eff = __getEffectiveExcludeSelectors();
|
|
11767
|
+
if (!eff.length || !el || !el.closest) return false;
|
|
11742
11768
|
try {
|
|
11743
|
-
return
|
|
11769
|
+
return eff.some((sel) => !!el.closest(sel));
|
|
11744
11770
|
} catch {
|
|
11745
11771
|
return false;
|
|
11746
11772
|
}
|
|
@@ -11835,8 +11861,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
11835
11861
|
if (!scope || !scope.querySelectorAll) return [];
|
|
11836
11862
|
|
|
11837
11863
|
// Cache shadow root discovery per root to avoid repeated querySelectorAll('*') walks.
|
|
11838
|
-
// IMPORTANT: do not cache when
|
|
11839
|
-
|
|
11864
|
+
// IMPORTANT: do not cache when the effective exclude list (global
|
|
11865
|
+
// ∪ active rule-scoped excludes) is non-empty -- different rules
|
|
11866
|
+
// may have different effective lists and must not share results.
|
|
11867
|
+
if (!__getEffectiveExcludeSelectors().length && __shadowRootsByRoot) {
|
|
11840
11868
|
try {
|
|
11841
11869
|
const cached = __shadowRootsByRoot.get(scope);
|
|
11842
11870
|
if (cached) {
|
|
@@ -11909,9 +11937,38 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
11909
11937
|
return results;
|
|
11910
11938
|
}
|
|
11911
11939
|
|
|
11940
|
+
const HARD_HIDDEN_REASONS = new Set([
|
|
11941
|
+
'displayNone',
|
|
11942
|
+
'hiddenAttr',
|
|
11943
|
+
'detailsClosed',
|
|
11944
|
+
'templateContent',
|
|
11945
|
+
'nonRenderedElement',
|
|
11946
|
+
'inputHidden',
|
|
11947
|
+
'visibilityHidden'
|
|
11948
|
+
]);
|
|
11949
|
+
|
|
11912
11950
|
function queryAllSmart(sel) {
|
|
11913
|
-
|
|
11914
|
-
|
|
11951
|
+
let list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
|
|
11952
|
+
|
|
11953
|
+
// Global hidden-content policy: skip nodes that are fully excluded from
|
|
11954
|
+
// rendered visibility by default (unless includeHiddenElements:true).
|
|
11955
|
+
if (!includeHiddenElements) {
|
|
11956
|
+
list = list.filter((el) => {
|
|
11957
|
+
try {
|
|
11958
|
+
const vis = isAccTreeEligible(el);
|
|
11959
|
+
if (!vis || vis.eligible !== false) return true;
|
|
11960
|
+
const reasons = Array.isArray(vis.reasons) ? vis.reasons : [];
|
|
11961
|
+
for (const r of reasons) {
|
|
11962
|
+
if (HARD_HIDDEN_REASONS.has(r)) return false;
|
|
11963
|
+
}
|
|
11964
|
+
return true;
|
|
11965
|
+
} catch {
|
|
11966
|
+
return true;
|
|
11967
|
+
}
|
|
11968
|
+
});
|
|
11969
|
+
}
|
|
11970
|
+
|
|
11971
|
+
return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
|
|
11915
11972
|
}
|
|
11916
11973
|
|
|
11917
11974
|
// -------------------------------------------------------------------------
|
|
@@ -11945,10 +12002,11 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
11945
12002
|
function __getSelectorCacheForOpts() {
|
|
11946
12003
|
if (!__selectorCache) return null;
|
|
11947
12004
|
try {
|
|
11948
|
-
|
|
12005
|
+
const key = __getSelectorOptsKey();
|
|
12006
|
+
let wm = __selectorCache.get(key);
|
|
11949
12007
|
if (!(wm instanceof WeakMap)) {
|
|
11950
12008
|
wm = new WeakMap();
|
|
11951
|
-
__selectorCache.set(
|
|
12009
|
+
__selectorCache.set(key, wm);
|
|
11952
12010
|
}
|
|
11953
12011
|
return wm;
|
|
11954
12012
|
} catch {
|
|
@@ -14359,7 +14417,8 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
14359
14417
|
return createSelectorUniqIndex();
|
|
14360
14418
|
}
|
|
14361
14419
|
|
|
14362
|
-
const
|
|
14420
|
+
const key = __getSelectorOptsKey();
|
|
14421
|
+
const cached = perScope.get(key);
|
|
14363
14422
|
if (cached) {
|
|
14364
14423
|
__perfInc('uniqIndex.hit');
|
|
14365
14424
|
return cached;
|
|
@@ -14368,7 +14427,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
14368
14427
|
__perfInc('uniqIndex.miss');
|
|
14369
14428
|
const idx = createSelectorUniqIndex();
|
|
14370
14429
|
try {
|
|
14371
|
-
perScope.set(
|
|
14430
|
+
perScope.set(key, idx);
|
|
14372
14431
|
} catch { /* ignore */
|
|
14373
14432
|
}
|
|
14374
14433
|
__perfInc('uniqIndex.build');
|
|
@@ -14878,6 +14937,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
14878
14937
|
isAccTreeEligible,
|
|
14879
14938
|
isDomVisibleEligible,
|
|
14880
14939
|
|
|
14940
|
+
// Engine-internal: sets which rule's rule-scoped excludeSelectors
|
|
14941
|
+
// (engineOptions.rules[ruleId].excludeSelectors) are currently in
|
|
14942
|
+
// effect. Called by dom-runner.js before each rule invocation, not
|
|
14943
|
+
// intended for use by rule implementations.
|
|
14944
|
+
__setActiveRuleExcludeSelectors,
|
|
14945
|
+
|
|
14881
14946
|
// Eligibility info wrapper
|
|
14882
14947
|
getEligibilityInfo,
|
|
14883
14948
|
|
|
@@ -15097,6 +15162,9 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
15097
15162
|
|
|
15098
15163
|
// Default on: opt OUT with `includeShadowDom: false`, not opt in.
|
|
15099
15164
|
const includeShadowDom = !(engineOptionsResolved && engineOptionsResolved.includeShadowDom === false);
|
|
15165
|
+
// Default off: hidden/collapsed content is excluded from rule evaluation
|
|
15166
|
+
// unless the caller explicitly opts in.
|
|
15167
|
+
const includeHiddenElements = !!(engineOptionsResolved && engineOptionsResolved.includeHiddenElements === true);
|
|
15100
15168
|
const excludeSelectors = normalizeSelectorList(engineOptionsResolved && engineOptionsResolved.excludeSelectors);
|
|
15101
15169
|
|
|
15102
15170
|
const url = pageUrl || (document.location && document.location.href) || null;
|
|
@@ -15112,6 +15180,7 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
15112
15180
|
window,
|
|
15113
15181
|
root: roots,
|
|
15114
15182
|
includeShadowDom,
|
|
15183
|
+
includeHiddenElements,
|
|
15115
15184
|
excludeSelectors,
|
|
15116
15185
|
// Optional perf counters (bench/debug only). Deterministic and per-run.
|
|
15117
15186
|
perfStats: !!(engineOptionsResolved && engineOptionsResolved.perfStats)
|
|
@@ -15291,6 +15360,15 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
15291
15360
|
? engineOptionsResolved.rules[defResolved.ruleId]
|
|
15292
15361
|
: null;
|
|
15293
15362
|
|
|
15363
|
+
// Rule-scoped excludeSelectors (engineOptions.rules[ruleId].excludeSelectors)
|
|
15364
|
+
// apply on top of the global excludeSelectors for exactly this rule's
|
|
15365
|
+
// applicability check + run, then are cleared once this rule is done.
|
|
15366
|
+
// Safe because rule execution below is synchronous and one rule at a
|
|
15367
|
+
// time -- sharedHelpers is reused across all rules in this loop.
|
|
15368
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
15369
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(ruleConfig && ruleConfig.excludeSelectors);
|
|
15370
|
+
}
|
|
15371
|
+
|
|
15294
15372
|
const ctx = {
|
|
15295
15373
|
document,
|
|
15296
15374
|
window,
|
|
@@ -15361,6 +15439,14 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
15361
15439
|
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
15362
15440
|
}
|
|
15363
15441
|
|
|
15442
|
+
// Composite rollups below carry no occurrences/nodes of their own, so
|
|
15443
|
+
// they never exercise rule-scoped excludes -- but clear the "active
|
|
15444
|
+
// rule" state on sharedHelpers regardless, so nothing after this point
|
|
15445
|
+
// (composite aggregation, perf stats) can observe a stale rule's excludes.
|
|
15446
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
15447
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(null);
|
|
15448
|
+
}
|
|
15449
|
+
|
|
15364
15450
|
// =========================
|
|
15365
15451
|
// Composite rule aggregation (data-only rollups)
|
|
15366
15452
|
// =========================
|
|
@@ -43007,16 +43093,41 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
43007
43093
|
})();
|
|
43008
43094
|
// Default on: opt OUT with `includeShadowDom: false`, not opt in.
|
|
43009
43095
|
const includeShadowDom = !(opts && opts.includeShadowDom === false);
|
|
43096
|
+
// Default off: by default, helper queries skip structurally/CSS-hidden
|
|
43097
|
+
// subtrees (display:none, [hidden], closed <details>, etc.). Callers can
|
|
43098
|
+
// opt out with includeHiddenElements:true.
|
|
43099
|
+
const includeHiddenElements = !!(opts && opts.includeHiddenElements === true);
|
|
43010
43100
|
const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
|
|
43011
43101
|
|
|
43102
|
+
// Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
|
|
43103
|
+
// by dom-runner.js immediately before invoking each rule's applicability/
|
|
43104
|
+
// run function via __setActiveRuleExcludeSelectors(). Safe as mutable
|
|
43105
|
+
// closure state because rule execution is synchronous and single-rule-
|
|
43106
|
+
// at-a-time: exactly one rule's excludes are ever "active" at once.
|
|
43107
|
+
var __activeRuleExcludeSelectors = [];
|
|
43108
|
+
|
|
43109
|
+
function __getEffectiveExcludeSelectors() {
|
|
43110
|
+
return __activeRuleExcludeSelectors.length
|
|
43111
|
+
? excludeSelectors.concat(__activeRuleExcludeSelectors)
|
|
43112
|
+
: excludeSelectors;
|
|
43113
|
+
}
|
|
43114
|
+
|
|
43115
|
+
function __setActiveRuleExcludeSelectors(list) {
|
|
43116
|
+
__activeRuleExcludeSelectors = normalizeSelectorList(list);
|
|
43117
|
+
}
|
|
43118
|
+
|
|
43012
43119
|
// 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
|
-
|
|
43120
|
+
// selector strings) depend on includeShadowDom/the effective exclude
|
|
43121
|
+
// list, since those change which elements are considered when checking
|
|
43122
|
+
// uniqueness. The underlying storage is shared across createDomHelpers()
|
|
43123
|
+
// calls on the same window/document (see __domSharedCache below), so a
|
|
43124
|
+
// run -- or a rule with its own rule-scoped excludes -- must not
|
|
43125
|
+
// read/write another run/rule's cached selectors. This key partitions
|
|
43126
|
+
// those caches per effective option set; recomputed per call (not a
|
|
43127
|
+
// constant) since the effective list changes as the active rule changes.
|
|
43128
|
+
function __getSelectorOptsKey() {
|
|
43129
|
+
return (includeShadowDom ? 'sd1' : 'sd0') + '|' + __getEffectiveExcludeSelectors().slice().sort().join(',');
|
|
43130
|
+
}
|
|
43020
43131
|
|
|
43021
43132
|
// -------------------------------------------------------------------------
|
|
43022
43133
|
// Per-run shared caches (DOM helpers)
|
|
@@ -43778,9 +43889,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
43778
43889
|
|
|
43779
43890
|
|
|
43780
43891
|
function isExcluded(el) {
|
|
43781
|
-
|
|
43892
|
+
const eff = __getEffectiveExcludeSelectors();
|
|
43893
|
+
if (!eff.length || !el || !el.closest) return false;
|
|
43782
43894
|
try {
|
|
43783
|
-
return
|
|
43895
|
+
return eff.some((sel) => !!el.closest(sel));
|
|
43784
43896
|
} catch {
|
|
43785
43897
|
return false;
|
|
43786
43898
|
}
|
|
@@ -43875,8 +43987,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
43875
43987
|
if (!scope || !scope.querySelectorAll) return [];
|
|
43876
43988
|
|
|
43877
43989
|
// Cache shadow root discovery per root to avoid repeated querySelectorAll('*') walks.
|
|
43878
|
-
// IMPORTANT: do not cache when
|
|
43879
|
-
|
|
43990
|
+
// IMPORTANT: do not cache when the effective exclude list (global
|
|
43991
|
+
// ∪ active rule-scoped excludes) is non-empty -- different rules
|
|
43992
|
+
// may have different effective lists and must not share results.
|
|
43993
|
+
if (!__getEffectiveExcludeSelectors().length && __shadowRootsByRoot) {
|
|
43880
43994
|
try {
|
|
43881
43995
|
const cached = __shadowRootsByRoot.get(scope);
|
|
43882
43996
|
if (cached) {
|
|
@@ -43949,9 +44063,38 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
43949
44063
|
return results;
|
|
43950
44064
|
}
|
|
43951
44065
|
|
|
44066
|
+
const HARD_HIDDEN_REASONS = new Set([
|
|
44067
|
+
'displayNone',
|
|
44068
|
+
'hiddenAttr',
|
|
44069
|
+
'detailsClosed',
|
|
44070
|
+
'templateContent',
|
|
44071
|
+
'nonRenderedElement',
|
|
44072
|
+
'inputHidden',
|
|
44073
|
+
'visibilityHidden'
|
|
44074
|
+
]);
|
|
44075
|
+
|
|
43952
44076
|
function queryAllSmart(sel) {
|
|
43953
|
-
|
|
43954
|
-
|
|
44077
|
+
let list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
|
|
44078
|
+
|
|
44079
|
+
// Global hidden-content policy: skip nodes that are fully excluded from
|
|
44080
|
+
// rendered visibility by default (unless includeHiddenElements:true).
|
|
44081
|
+
if (!includeHiddenElements) {
|
|
44082
|
+
list = list.filter((el) => {
|
|
44083
|
+
try {
|
|
44084
|
+
const vis = isAccTreeEligible(el);
|
|
44085
|
+
if (!vis || vis.eligible !== false) return true;
|
|
44086
|
+
const reasons = Array.isArray(vis.reasons) ? vis.reasons : [];
|
|
44087
|
+
for (const r of reasons) {
|
|
44088
|
+
if (HARD_HIDDEN_REASONS.has(r)) return false;
|
|
44089
|
+
}
|
|
44090
|
+
return true;
|
|
44091
|
+
} catch {
|
|
44092
|
+
return true;
|
|
44093
|
+
}
|
|
44094
|
+
});
|
|
44095
|
+
}
|
|
44096
|
+
|
|
44097
|
+
return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
|
|
43955
44098
|
}
|
|
43956
44099
|
|
|
43957
44100
|
// -------------------------------------------------------------------------
|
|
@@ -43985,10 +44128,11 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
43985
44128
|
function __getSelectorCacheForOpts() {
|
|
43986
44129
|
if (!__selectorCache) return null;
|
|
43987
44130
|
try {
|
|
43988
|
-
|
|
44131
|
+
const key = __getSelectorOptsKey();
|
|
44132
|
+
let wm = __selectorCache.get(key);
|
|
43989
44133
|
if (!(wm instanceof WeakMap)) {
|
|
43990
44134
|
wm = new WeakMap();
|
|
43991
|
-
__selectorCache.set(
|
|
44135
|
+
__selectorCache.set(key, wm);
|
|
43992
44136
|
}
|
|
43993
44137
|
return wm;
|
|
43994
44138
|
} catch {
|
|
@@ -46399,7 +46543,8 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
46399
46543
|
return createSelectorUniqIndex();
|
|
46400
46544
|
}
|
|
46401
46545
|
|
|
46402
|
-
const
|
|
46546
|
+
const key = __getSelectorOptsKey();
|
|
46547
|
+
const cached = perScope.get(key);
|
|
46403
46548
|
if (cached) {
|
|
46404
46549
|
__perfInc('uniqIndex.hit');
|
|
46405
46550
|
return cached;
|
|
@@ -46408,7 +46553,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
46408
46553
|
__perfInc('uniqIndex.miss');
|
|
46409
46554
|
const idx = createSelectorUniqIndex();
|
|
46410
46555
|
try {
|
|
46411
|
-
perScope.set(
|
|
46556
|
+
perScope.set(key, idx);
|
|
46412
46557
|
} catch { /* ignore */
|
|
46413
46558
|
}
|
|
46414
46559
|
__perfInc('uniqIndex.build');
|
|
@@ -46918,6 +47063,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
46918
47063
|
isAccTreeEligible,
|
|
46919
47064
|
isDomVisibleEligible,
|
|
46920
47065
|
|
|
47066
|
+
// Engine-internal: sets which rule's rule-scoped excludeSelectors
|
|
47067
|
+
// (engineOptions.rules[ruleId].excludeSelectors) are currently in
|
|
47068
|
+
// effect. Called by dom-runner.js before each rule invocation, not
|
|
47069
|
+
// intended for use by rule implementations.
|
|
47070
|
+
__setActiveRuleExcludeSelectors,
|
|
47071
|
+
|
|
46921
47072
|
// Eligibility info wrapper
|
|
46922
47073
|
getEligibilityInfo,
|
|
46923
47074
|
|
|
@@ -47137,6 +47288,9 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
47137
47288
|
|
|
47138
47289
|
// Default on: opt OUT with `includeShadowDom: false`, not opt in.
|
|
47139
47290
|
const includeShadowDom = !(engineOptionsResolved && engineOptionsResolved.includeShadowDom === false);
|
|
47291
|
+
// Default off: hidden/collapsed content is excluded from rule evaluation
|
|
47292
|
+
// unless the caller explicitly opts in.
|
|
47293
|
+
const includeHiddenElements = !!(engineOptionsResolved && engineOptionsResolved.includeHiddenElements === true);
|
|
47140
47294
|
const excludeSelectors = normalizeSelectorList(engineOptionsResolved && engineOptionsResolved.excludeSelectors);
|
|
47141
47295
|
|
|
47142
47296
|
const url = pageUrl || (document.location && document.location.href) || null;
|
|
@@ -47152,6 +47306,7 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
47152
47306
|
window,
|
|
47153
47307
|
root: roots,
|
|
47154
47308
|
includeShadowDom,
|
|
47309
|
+
includeHiddenElements,
|
|
47155
47310
|
excludeSelectors,
|
|
47156
47311
|
// Optional perf counters (bench/debug only). Deterministic and per-run.
|
|
47157
47312
|
perfStats: !!(engineOptionsResolved && engineOptionsResolved.perfStats)
|
|
@@ -47331,6 +47486,15 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
47331
47486
|
? engineOptionsResolved.rules[defResolved.ruleId]
|
|
47332
47487
|
: null;
|
|
47333
47488
|
|
|
47489
|
+
// Rule-scoped excludeSelectors (engineOptions.rules[ruleId].excludeSelectors)
|
|
47490
|
+
// apply on top of the global excludeSelectors for exactly this rule's
|
|
47491
|
+
// applicability check + run, then are cleared once this rule is done.
|
|
47492
|
+
// Safe because rule execution below is synchronous and one rule at a
|
|
47493
|
+
// time -- sharedHelpers is reused across all rules in this loop.
|
|
47494
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
47495
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(ruleConfig && ruleConfig.excludeSelectors);
|
|
47496
|
+
}
|
|
47497
|
+
|
|
47334
47498
|
const ctx = {
|
|
47335
47499
|
document,
|
|
47336
47500
|
window,
|
|
@@ -47401,6 +47565,14 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
47401
47565
|
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
47402
47566
|
}
|
|
47403
47567
|
|
|
47568
|
+
// Composite rollups below carry no occurrences/nodes of their own, so
|
|
47569
|
+
// they never exercise rule-scoped excludes -- but clear the "active
|
|
47570
|
+
// rule" state on sharedHelpers regardless, so nothing after this point
|
|
47571
|
+
// (composite aggregation, perf stats) can observe a stale rule's excludes.
|
|
47572
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
47573
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(null);
|
|
47574
|
+
}
|
|
47575
|
+
|
|
47404
47576
|
// =========================
|
|
47405
47577
|
// Composite rule aggregation (data-only rollups)
|
|
47406
47578
|
// =========================
|
|
@@ -75002,16 +75174,41 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
75002
75174
|
})();
|
|
75003
75175
|
// Default on: opt OUT with `includeShadowDom: false`, not opt in.
|
|
75004
75176
|
const includeShadowDom = !(opts && opts.includeShadowDom === false);
|
|
75177
|
+
// Default off: by default, helper queries skip structurally/CSS-hidden
|
|
75178
|
+
// subtrees (display:none, [hidden], closed <details>, etc.). Callers can
|
|
75179
|
+
// opt out with includeHiddenElements:true.
|
|
75180
|
+
const includeHiddenElements = !!(opts && opts.includeHiddenElements === true);
|
|
75005
75181
|
const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
|
|
75006
75182
|
|
|
75183
|
+
// Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
|
|
75184
|
+
// by dom-runner.js immediately before invoking each rule's applicability/
|
|
75185
|
+
// run function via __setActiveRuleExcludeSelectors(). Safe as mutable
|
|
75186
|
+
// closure state because rule execution is synchronous and single-rule-
|
|
75187
|
+
// at-a-time: exactly one rule's excludes are ever "active" at once.
|
|
75188
|
+
var __activeRuleExcludeSelectors = [];
|
|
75189
|
+
|
|
75190
|
+
function __getEffectiveExcludeSelectors() {
|
|
75191
|
+
return __activeRuleExcludeSelectors.length
|
|
75192
|
+
? excludeSelectors.concat(__activeRuleExcludeSelectors)
|
|
75193
|
+
: excludeSelectors;
|
|
75194
|
+
}
|
|
75195
|
+
|
|
75196
|
+
function __setActiveRuleExcludeSelectors(list) {
|
|
75197
|
+
__activeRuleExcludeSelectors = normalizeSelectorList(list);
|
|
75198
|
+
}
|
|
75199
|
+
|
|
75007
75200
|
// 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
|
-
|
|
75201
|
+
// selector strings) depend on includeShadowDom/the effective exclude
|
|
75202
|
+
// list, since those change which elements are considered when checking
|
|
75203
|
+
// uniqueness. The underlying storage is shared across createDomHelpers()
|
|
75204
|
+
// calls on the same window/document (see __domSharedCache below), so a
|
|
75205
|
+
// run -- or a rule with its own rule-scoped excludes -- must not
|
|
75206
|
+
// read/write another run/rule's cached selectors. This key partitions
|
|
75207
|
+
// those caches per effective option set; recomputed per call (not a
|
|
75208
|
+
// constant) since the effective list changes as the active rule changes.
|
|
75209
|
+
function __getSelectorOptsKey() {
|
|
75210
|
+
return (includeShadowDom ? 'sd1' : 'sd0') + '|' + __getEffectiveExcludeSelectors().slice().sort().join(',');
|
|
75211
|
+
}
|
|
75015
75212
|
|
|
75016
75213
|
// -------------------------------------------------------------------------
|
|
75017
75214
|
// Per-run shared caches (DOM helpers)
|
|
@@ -75773,9 +75970,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
75773
75970
|
|
|
75774
75971
|
|
|
75775
75972
|
function isExcluded(el) {
|
|
75776
|
-
|
|
75973
|
+
const eff = __getEffectiveExcludeSelectors();
|
|
75974
|
+
if (!eff.length || !el || !el.closest) return false;
|
|
75777
75975
|
try {
|
|
75778
|
-
return
|
|
75976
|
+
return eff.some((sel) => !!el.closest(sel));
|
|
75779
75977
|
} catch {
|
|
75780
75978
|
return false;
|
|
75781
75979
|
}
|
|
@@ -75870,8 +76068,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
75870
76068
|
if (!scope || !scope.querySelectorAll) return [];
|
|
75871
76069
|
|
|
75872
76070
|
// Cache shadow root discovery per root to avoid repeated querySelectorAll('*') walks.
|
|
75873
|
-
// IMPORTANT: do not cache when
|
|
75874
|
-
|
|
76071
|
+
// IMPORTANT: do not cache when the effective exclude list (global
|
|
76072
|
+
// ∪ active rule-scoped excludes) is non-empty -- different rules
|
|
76073
|
+
// may have different effective lists and must not share results.
|
|
76074
|
+
if (!__getEffectiveExcludeSelectors().length && __shadowRootsByRoot) {
|
|
75875
76075
|
try {
|
|
75876
76076
|
const cached = __shadowRootsByRoot.get(scope);
|
|
75877
76077
|
if (cached) {
|
|
@@ -75944,9 +76144,38 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
75944
76144
|
return results;
|
|
75945
76145
|
}
|
|
75946
76146
|
|
|
76147
|
+
const HARD_HIDDEN_REASONS = new Set([
|
|
76148
|
+
'displayNone',
|
|
76149
|
+
'hiddenAttr',
|
|
76150
|
+
'detailsClosed',
|
|
76151
|
+
'templateContent',
|
|
76152
|
+
'nonRenderedElement',
|
|
76153
|
+
'inputHidden',
|
|
76154
|
+
'visibilityHidden'
|
|
76155
|
+
]);
|
|
76156
|
+
|
|
75947
76157
|
function queryAllSmart(sel) {
|
|
75948
|
-
|
|
75949
|
-
|
|
76158
|
+
let list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
|
|
76159
|
+
|
|
76160
|
+
// Global hidden-content policy: skip nodes that are fully excluded from
|
|
76161
|
+
// rendered visibility by default (unless includeHiddenElements:true).
|
|
76162
|
+
if (!includeHiddenElements) {
|
|
76163
|
+
list = list.filter((el) => {
|
|
76164
|
+
try {
|
|
76165
|
+
const vis = isAccTreeEligible(el);
|
|
76166
|
+
if (!vis || vis.eligible !== false) return true;
|
|
76167
|
+
const reasons = Array.isArray(vis.reasons) ? vis.reasons : [];
|
|
76168
|
+
for (const r of reasons) {
|
|
76169
|
+
if (HARD_HIDDEN_REASONS.has(r)) return false;
|
|
76170
|
+
}
|
|
76171
|
+
return true;
|
|
76172
|
+
} catch {
|
|
76173
|
+
return true;
|
|
76174
|
+
}
|
|
76175
|
+
});
|
|
76176
|
+
}
|
|
76177
|
+
|
|
76178
|
+
return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
|
|
75950
76179
|
}
|
|
75951
76180
|
|
|
75952
76181
|
// -------------------------------------------------------------------------
|
|
@@ -75980,10 +76209,11 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
75980
76209
|
function __getSelectorCacheForOpts() {
|
|
75981
76210
|
if (!__selectorCache) return null;
|
|
75982
76211
|
try {
|
|
75983
|
-
|
|
76212
|
+
const key = __getSelectorOptsKey();
|
|
76213
|
+
let wm = __selectorCache.get(key);
|
|
75984
76214
|
if (!(wm instanceof WeakMap)) {
|
|
75985
76215
|
wm = new WeakMap();
|
|
75986
|
-
__selectorCache.set(
|
|
76216
|
+
__selectorCache.set(key, wm);
|
|
75987
76217
|
}
|
|
75988
76218
|
return wm;
|
|
75989
76219
|
} catch {
|
|
@@ -78394,7 +78624,8 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
78394
78624
|
return createSelectorUniqIndex();
|
|
78395
78625
|
}
|
|
78396
78626
|
|
|
78397
|
-
const
|
|
78627
|
+
const key = __getSelectorOptsKey();
|
|
78628
|
+
const cached = perScope.get(key);
|
|
78398
78629
|
if (cached) {
|
|
78399
78630
|
__perfInc('uniqIndex.hit');
|
|
78400
78631
|
return cached;
|
|
@@ -78403,7 +78634,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
78403
78634
|
__perfInc('uniqIndex.miss');
|
|
78404
78635
|
const idx = createSelectorUniqIndex();
|
|
78405
78636
|
try {
|
|
78406
|
-
perScope.set(
|
|
78637
|
+
perScope.set(key, idx);
|
|
78407
78638
|
} catch { /* ignore */
|
|
78408
78639
|
}
|
|
78409
78640
|
__perfInc('uniqIndex.build');
|
|
@@ -78913,6 +79144,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
|
|
|
78913
79144
|
isAccTreeEligible,
|
|
78914
79145
|
isDomVisibleEligible,
|
|
78915
79146
|
|
|
79147
|
+
// Engine-internal: sets which rule's rule-scoped excludeSelectors
|
|
79148
|
+
// (engineOptions.rules[ruleId].excludeSelectors) are currently in
|
|
79149
|
+
// effect. Called by dom-runner.js before each rule invocation, not
|
|
79150
|
+
// intended for use by rule implementations.
|
|
79151
|
+
__setActiveRuleExcludeSelectors,
|
|
79152
|
+
|
|
78916
79153
|
// Eligibility info wrapper
|
|
78917
79154
|
getEligibilityInfo,
|
|
78918
79155
|
|
|
@@ -79132,6 +79369,9 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
79132
79369
|
|
|
79133
79370
|
// Default on: opt OUT with `includeShadowDom: false`, not opt in.
|
|
79134
79371
|
const includeShadowDom = !(engineOptionsResolved && engineOptionsResolved.includeShadowDom === false);
|
|
79372
|
+
// Default off: hidden/collapsed content is excluded from rule evaluation
|
|
79373
|
+
// unless the caller explicitly opts in.
|
|
79374
|
+
const includeHiddenElements = !!(engineOptionsResolved && engineOptionsResolved.includeHiddenElements === true);
|
|
79135
79375
|
const excludeSelectors = normalizeSelectorList(engineOptionsResolved && engineOptionsResolved.excludeSelectors);
|
|
79136
79376
|
|
|
79137
79377
|
const url = pageUrl || (document.location && document.location.href) || null;
|
|
@@ -79147,6 +79387,7 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
79147
79387
|
window,
|
|
79148
79388
|
root: roots,
|
|
79149
79389
|
includeShadowDom,
|
|
79390
|
+
includeHiddenElements,
|
|
79150
79391
|
excludeSelectors,
|
|
79151
79392
|
// Optional perf counters (bench/debug only). Deterministic and per-run.
|
|
79152
79393
|
perfStats: !!(engineOptionsResolved && engineOptionsResolved.perfStats)
|
|
@@ -79326,6 +79567,15 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
79326
79567
|
? engineOptionsResolved.rules[defResolved.ruleId]
|
|
79327
79568
|
: null;
|
|
79328
79569
|
|
|
79570
|
+
// Rule-scoped excludeSelectors (engineOptions.rules[ruleId].excludeSelectors)
|
|
79571
|
+
// apply on top of the global excludeSelectors for exactly this rule's
|
|
79572
|
+
// applicability check + run, then are cleared once this rule is done.
|
|
79573
|
+
// Safe because rule execution below is synchronous and one rule at a
|
|
79574
|
+
// time -- sharedHelpers is reused across all rules in this loop.
|
|
79575
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
79576
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(ruleConfig && ruleConfig.excludeSelectors);
|
|
79577
|
+
}
|
|
79578
|
+
|
|
79329
79579
|
const ctx = {
|
|
79330
79580
|
document,
|
|
79331
79581
|
window,
|
|
@@ -79396,6 +79646,14 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
|
|
|
79396
79646
|
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
79397
79647
|
}
|
|
79398
79648
|
|
|
79649
|
+
// Composite rollups below carry no occurrences/nodes of their own, so
|
|
79650
|
+
// they never exercise rule-scoped excludes -- but clear the "active
|
|
79651
|
+
// rule" state on sharedHelpers regardless, so nothing after this point
|
|
79652
|
+
// (composite aggregation, perf stats) can observe a stale rule's excludes.
|
|
79653
|
+
if (typeof sharedHelpers.__setActiveRuleExcludeSelectors === 'function') {
|
|
79654
|
+
sharedHelpers.__setActiveRuleExcludeSelectors(null);
|
|
79655
|
+
}
|
|
79656
|
+
|
|
79399
79657
|
// =========================
|
|
79400
79658
|
// Composite rule aggregation (data-only rollups)
|
|
79401
79659
|
// =========================
|