Skip to content

Commit d07b895

Browse files
committed
fix: fixed YouTube Music Background Play
1 parent ea82026 commit d07b895

9 files changed

Lines changed: 910 additions & 271 deletions

File tree

src/lib/filters/README.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Filters Module
2+
3+
A custom CSS selector matching and DOM observation engine with Shadow DOM support and uBlock Origin procedural cosmetic filter extensions.
4+
5+
## API
6+
7+
### `querySelectorAll(root, selector)`
8+
9+
Returns all elements under `root` matching `selector`, including inside shadow roots.
10+
11+
```ts
12+
import { querySelectorAll } from './filters'
13+
14+
const elements = querySelectorAll(document.body, '.promoted:has-text("Ad")')
15+
```
16+
17+
### `querySelector(root, selector)`
18+
19+
Returns the first matching element, or `null`.
20+
21+
```ts
22+
const el = querySelector(document.body, '#sidebar .widget:upward(1)')
23+
```
24+
25+
### `observe(root, selector, options)`
26+
27+
Watches `root` for matching elements — both existing and dynamically added. Returns a cleanup function.
28+
29+
```ts
30+
const cleanup = observe(document.body, '.ad-banner', {
31+
onMatch: (elements) => {
32+
elements.forEach((el) => el.remove())
33+
},
34+
})
35+
36+
// With onUnmatch (for conditional selectors)
37+
const cleanup = observe(document.body, '.widget:matches-path(/^\\/r\\//)', {
38+
onMatch: (elements) => {
39+
elements.forEach((el) => (el.style.display = 'none'))
40+
},
41+
onUnmatch: (elements) => {
42+
elements.forEach((el) => (el.style.display = ''))
43+
},
44+
})
45+
46+
// Stop observing
47+
cleanup()
48+
```
49+
50+
Behavior:
51+
52+
- Initial scan results are delivered synchronously
53+
- Dynamic mutations are batched via `requestAnimationFrame`
54+
- Each element is reported at most once (deduplicated via `WeakSet`)
55+
- Shadow roots are detected both on scan and via 500ms polling for lazy creation
56+
57+
## Supported Selectors
58+
59+
### Standard CSS
60+
61+
| Selector | Example |
62+
| --- | --- |
63+
| Tag | `div`, `span` |
64+
| Class | `.foo`, `.foo.bar` |
65+
| ID | `#myid` |
66+
| Universal | `*` |
67+
| Attribute | `[attr]`, `[attr=val]`, `[attr^=val]`, `[attr$=val]`, `[attr*=val]`, `[attr\|=val]` |
68+
| Descendant | `#parent .child` |
69+
| Child | `#parent > .child` |
70+
| Adjacent sibling | `.a + .b` |
71+
| General sibling | `.a ~ .b` |
72+
| Comma groups | `.a, .b` |
73+
| `:not()` | `:not(.hidden)` |
74+
| `:is()` / `:where()` | `:is(.a, .b)` |
75+
| `:has()` | `div:has(.inner)` |
76+
| `:first-child` / `:last-child` / `:only-child` | |
77+
| `:nth-child()` / `:nth-last-child()` | `:nth-child(2n+1)`, `:nth-child(odd)` |
78+
| `:nth-of-type()` / `:nth-last-of-type()` | |
79+
| `:first-of-type` / `:last-of-type` / `:only-of-type` | |
80+
| `:root` / `:empty` | |
81+
82+
### Extended (uBlock Origin procedural cosmetic filters)
83+
84+
| Selector | Description |
85+
| --- | --- |
86+
| `:has-text(text)` | Matches if the element's `textContent` contains `text`. Supports regex: `:has-text(/pattern/flags)`. |
87+
| `:upward(n)` | Returns the ancestor `n` levels above the matched element. **Must be terminal** (see below). |
88+
| `:upward(selector)` | Returns the closest ancestor matching `selector`. **Must be terminal** (see below). |
89+
| `:matches-media(query)` | Matches only when the media query is true. Re-evaluated on viewport changes. |
90+
| `:matches-path(path)` | Matches only when `location.pathname + location.search` contains `path`. Supports regex. Re-evaluated on SPA navigation. |
91+
92+
`:matches-media` and `:matches-path` are **conditional**`observe()` automatically re-evaluates them when the environment changes and fires `onUnmatch` when they stop matching.
93+
94+
**`:upward()` must be terminal** — it cannot be followed by a combinator (`+`, `~`, `>`, or descendant space). The matching engine evaluates selectors right-to-left, so `:upward()` can only redirect the final match target. Selectors like `.child:upward(section) + hr` will throw an error at parse time. Use `:has()` instead for sibling selectors: `section:has(.child) + hr`.
95+
96+
## Architecture
97+
98+
```txt
99+
index.ts Public API re-exports
100+
101+
├── query.ts querySelectorAll / querySelector
102+
│ │
103+
│ └── matches.ts Core matching engine
104+
105+
└── observe.ts MutationObserver-based watcher
106+
107+
└── matches.ts
108+
```
109+
110+
### matches.ts — Matching Engine
111+
112+
Evaluates a single element against a parsed [css-what](https://github.com/fb55/css-what) AST.
113+
114+
- **Right-to-left matching**: Tokens are consumed from the rightmost simple selector, then combinators walk up/across the DOM tree.
115+
- **Return type**: `Element | null`. Normally returns the input element on match. `:upward()` redirects the return to an ancestor, so callers should use the returned element rather than the input.
116+
- **Shadow DOM**: The `getParent()` helper crosses shadow boundaries transparently — when an element's parent is a `ShadowRoot`, it jumps to `shadowRoot.host`.
117+
118+
### query.ts — Static Queries
119+
120+
Wraps `matches()` with a depth-first DOM walker (`walkDOM`) that enters shadow roots. Straightforward: parse selector, walk tree, collect matches.
121+
122+
### observe.ts — Dynamic Observation
123+
124+
Layers reactivity on top of `matches()`:
125+
126+
1. **AST splitting** — The parsed selector groups are classified as *unconditional* (pure DOM) or *conditional* (contains `:matches-media` / `:matches-path`). Unconditional elements are tracked in a `WeakSet` (fire-and-forget). Conditional elements are tracked in a `Set` (iterable for re-evaluation).
127+
128+
2. **MutationObserver** — Watches `childList` (subtree) and `attributes` (`class`, `id`) on the root and on discovered shadow roots.
129+
130+
3. **Shadow root polling** — A 500ms `setInterval` walks the tree looking for newly attached shadow roots (Web Components often create them asynchronously in `connectedCallback`).
131+
132+
4. **Environment listeners** — For conditional selectors:
133+
- Media queries: `matchMedia(query).addEventListener('change', ...)`
134+
- Path changes: monkey-patches `history.pushState` / `replaceState` and listens for `popstate`. Uses a shared reference-counted install so multiple `observe()` calls don't patch repeatedly.
135+
136+
5. **Re-evaluation** — When the environment changes, `reevaluateConditional()` runs two phases:
137+
- *Un-hide*: Remove elements from the conditional set that no longer match, call `onUnmatch`.
138+
- *Re-scan*: Walk the DOM for newly matching elements, call `onMatch`.
139+
140+
6. **Batching** — Mutations within the same frame are coalesced into a single `onMatch` call via `requestAnimationFrame`.

0 commit comments

Comments
 (0)