@thomasseanfahey/domq 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # domq
2
+
3
+ Query DOM relationships, not selectors.
4
+
5
+ `domq` is a small, ESM-first library for composable DOM traversal (relations) and filtering (predicates) with deterministic ordering and lazy evaluation.
6
+
7
+ It’s designed for browser extensions, testing tooling, scraping/automation scripts, and any code that needs *relationship-first* DOM queries that don’t fit cleanly into CSS selectors.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install domq
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```js
18
+ import { dq } from "domq";
19
+
20
+ const start = document.querySelector("#start");
21
+
22
+ const primary =
23
+ dq(start)
24
+ .closest("[data-card]")
25
+ .descendants()
26
+ .where(dq.attr("data-action").eq("primary"))
27
+ .one();
28
+
29
+ primary.click();
30
+ ```
31
+
32
+ ## Why not just selectors?
33
+
34
+ Selectors are excellent, but they are string-based and don’t express many *relational* queries cleanly, especially when your query starts from an element and needs multi-step traversal:
35
+
36
+ - “From this element, go up N ancestors, then traverse siblings, then descend, then pick the first match.”
37
+ - “Find the second ancestor that has at least 3 siblings matching a runtime predicate.”
38
+ - “Traverse across Shadow DOM boundaries (explicitly, not accidentally).”
39
+
40
+ `domq` gives you a readable pipeline with clear semantics.
41
+
42
+ ## Core concepts
43
+
44
+ ### Relations
45
+
46
+ Relations transform a set/sequence of elements into another ordered sequence.
47
+
48
+ Common relations:
49
+
50
+ - `.self()`
51
+ - `.parent()` / `.ancestors()`
52
+ - `.children()` / `.descendants()`
53
+ - `.siblings()` / `.followingSiblings()` / `.precedingSiblings()`
54
+ - `.closest(selector)`
55
+ - `.find(selector)`
56
+ - `.within(selector)`
57
+ - `.until(predicateOrSelector)`
58
+
59
+ ### Predicates
60
+
61
+ Predicates are composable tests applied via `.where(...)`.
62
+
63
+ ```js
64
+ dq.attr("data-x").eq("1")
65
+ dq.text().matches(/hello/i)
66
+ dq.value().includes("abc")
67
+ dq.matches("button, a")
68
+ dq.hasClass("active")
69
+ dq.role("dialog")
70
+ ```
71
+
72
+ Combine them:
73
+
74
+ ```js
75
+ const p = dq.and(
76
+ dq.matches("button"),
77
+ dq.attr("data-action").eq("primary")
78
+ );
79
+ ```
80
+
81
+ ### Selection (terminal operations)
82
+
83
+ Terminal operations evaluate lazily and can short-circuit:
84
+
85
+ - `.first()` → `Element | null`
86
+ - `.one()` → `Element` (throws with diagnostics if not exactly 1)
87
+ - `.maybeOne()` → `Element | null` (throws if more than 1)
88
+ - `.at(n)` / `.take(n)` / `.skip(n)` / `.slice(a,b)`
89
+ - `.exists()` / `.count()` / `.toArray()`
90
+ - `.unique()` / `.uniqueBy(fn)`
91
+
92
+ ## Deterministic semantics
93
+
94
+ - **Ordering** is deterministic and documented in code:
95
+ - `ancestors()` yields *nearest-first*
96
+ - `descendants()` yields *document order*
97
+ - `siblings()` yields *left-to-right DOM order*
98
+ - **Snapshot evaluation**: results reflect the DOM at evaluation time.
99
+ - **Duplicates**: pipelines may include duplicates; use `.unique()` explicitly.
100
+
101
+ ## Budgets (safety valves)
102
+
103
+ Guard traversal work in large DOMs:
104
+
105
+ ```js
106
+ const el = dq(start)
107
+ .budget({ maxNodes: 50_000, maxDepth: 100, maxMs: 25 })
108
+ .descendants()
109
+ .where(dq.matches("a"))
110
+ .first();
111
+ ```
112
+
113
+ ## Compile reusable queries
114
+
115
+ Turn a pipeline into a reusable function:
116
+
117
+ ```js
118
+ const getPrimaryCTA = dq.compile(q =>
119
+ q.closest("[data-card]")
120
+ .descendants()
121
+ .where(dq.attr("data-action").eq("primary"))
122
+ .first()
123
+ );
124
+
125
+ const cta = getPrimaryCTA(event.target);
126
+ ```
127
+
128
+ ## Diagnostics
129
+
130
+ - `.explain()` returns a readable pipeline description.
131
+ - `.debug(label)` and `.tap(fn)` allow inspection.
132
+ - `.one()` and `.maybeOne()` throw errors that include pipeline context and samples.
133
+
134
+ ## Shadow DOM (optional)
135
+
136
+ Shadow traversal is explicit via `domq/shadow`.
137
+
138
+ ```js
139
+ import { dq } from "domq";
140
+ import { shadow } from "domq/shadow";
141
+
142
+ const el = dq(start)
143
+ .use(shadow)
144
+ .composedAncestors()
145
+ .first();
146
+ ```
147
+
148
+ ## Extra predicates (optional)
149
+
150
+ Expensive predicates live in `domq/extra`.
151
+
152
+ ```js
153
+ import { dq } from "domq";
154
+ import { extra } from "domq/extra";
155
+
156
+ const visibleButtons = dq(document.body)
157
+ .use(extra)
158
+ .descendants()
159
+ .where(dq.matches("button"))
160
+ .where(dq.visible())
161
+ .toArray();
162
+ ```
163
+
164
+ ## Browser demo (no tooling)
165
+
166
+ Browsers can’t resolve bare specifiers like `"domq"` without a bundler. For a zero-build demo, use an **import map** pointing at local files.
167
+
168
+ ```html
169
+ <script type="importmap">
170
+ {
171
+ "imports": {
172
+ "domq": "./src/index.js",
173
+ "domq/shadow": "./src/shadow.js",
174
+ "domq/extra": "./src/extra.js"
175
+ }
176
+ }
177
+ </script>
178
+ <script type="module">
179
+ import { dq } from "domq";
180
+ console.log(dq(document.body).descendants().count());
181
+ </script>
182
+ ```
183
+
184
+ Serve the repo directory (modules don’t work reliably over `file://`):
185
+
186
+ ```bash
187
+ npx -y serve .
188
+ # or
189
+ python -m http.server 5173
190
+ ```
191
+
192
+ ## Development
193
+
194
+ ```bash
195
+ npm install
196
+ npm test
197
+ npm run build:types
198
+ npm run pack:check
199
+ ```
200
+
201
+ `prepublishOnly` runs tests and emits type declarations to `dist/`.
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,7 @@
1
+ export function dq(node: any): Query;
2
+ export namespace dq {
3
+ function all(iterable: any): Query;
4
+ function compile(builder: any): (start: any, options?: any) => any;
5
+ function use(plugin: any): void;
6
+ }
7
+ import { Query } from './query.js';
@@ -0,0 +1,32 @@
1
+ export function predicate(fn: any, describe?: string): any;
2
+ export function and(...preds: any[]): any;
3
+ export function or(...preds: any[]): any;
4
+ export function not(pred: any): any;
5
+ export function attr(name: any): ComparatorBuilder;
6
+ export function data(key: any): ComparatorBuilder;
7
+ export function dataset(key: any): ComparatorBuilder;
8
+ export function text(): ComparatorBuilder;
9
+ export function ownText(): ComparatorBuilder;
10
+ export function value(): ComparatorBuilder;
11
+ export function tag(tagName: any): any;
12
+ export function hasClass(className: any): any;
13
+ export function matches(selector: any): any;
14
+ export function role(roleName: any): any;
15
+ declare class ComparatorBuilder {
16
+ constructor(kind: any, getter: any);
17
+ _kind: any;
18
+ _getter: any;
19
+ exists(): any;
20
+ eq(value: any): any;
21
+ ne(value: any): any;
22
+ includes(substr: any): any;
23
+ startsWith(prefix: any): any;
24
+ endsWith(suffix: any): any;
25
+ matches(re: any): any;
26
+ truthy(): any;
27
+ gt(n: any): any;
28
+ gte(n: any): any;
29
+ lt(n: any): any;
30
+ lte(n: any): any;
31
+ }
32
+ export {};
@@ -0,0 +1,44 @@
1
+ export class Query {
2
+ static from(node: any): Query;
3
+ static fromAll(iterable: any): Query;
4
+ static installBaseRelations(relations: any): void;
5
+ constructor(iterableFactory: any, steps: any);
6
+ _iterableFactory: any;
7
+ _steps: any;
8
+ _budget: {};
9
+ _debugEnabled: boolean;
10
+ _debugLabel: any;
11
+ get(relation: any, ...args: any[]): Query;
12
+ where(pred: any): Query;
13
+ not(pred: any): Query;
14
+ unique(): Query;
15
+ uniqueBy(keyFn: any): Query;
16
+ reverse(): Query;
17
+ budget(budget: any): this;
18
+ debug(label?: boolean): this;
19
+ explain(): any;
20
+ first(): any;
21
+ exists(): boolean;
22
+ one(): any;
23
+ maybeOne(): any;
24
+ at(n: any): Query;
25
+ take(n: any): Query;
26
+ skip(n: any): Query;
27
+ slice(start: any, end: any): Query;
28
+ count(): number;
29
+ toArray(): any[];
30
+ map(fn: any): any[];
31
+ _clone(iterableFactory: any, steps: any): Query;
32
+ _evaluate(): {
33
+ ctx: {
34
+ budget: {};
35
+ debugEnabled: boolean;
36
+ debugLabel: any;
37
+ visitedNodes: number;
38
+ startedAt: number;
39
+ };
40
+ iterable: Generator<any, void, unknown>;
41
+ };
42
+ _maybeDebug(ctx: any, matchCount: any): void;
43
+ }
44
+ export function checkBudget(ctx: any): void;
@@ -0,0 +1,14 @@
1
+ export const self: any;
2
+ export const parent: any;
3
+ export const ancestors: any;
4
+ export const children: any;
5
+ export const descendants: any;
6
+ export const siblings: any;
7
+ export const followingSiblings: any;
8
+ export const precedingSiblings: any;
9
+ export const next: any;
10
+ export const prev: any;
11
+ export const closest: any;
12
+ export const find: any;
13
+ export const within: any;
14
+ export const until: any;
@@ -0,0 +1,7 @@
1
+ export function toElement(node: any): any;
2
+ export function toIterable(iterable: any): any;
3
+ export function describeElement(el: any): string;
4
+ export function normaliseText(s: any): string;
5
+ export function getOwnText(el: any): string;
6
+ export function getValue(el: any): string;
7
+ export function safeMatches(el: any, selector: any): any;
@@ -0,0 +1 @@
1
+ export { extra } from "./plugins/extra.js";
@@ -0,0 +1,3 @@
1
+ export { dq } from "./core/dq.js";
2
+ export { shadow } from "./plugins/shadow.js";
3
+ export { extra } from "./plugins/extra.js";
@@ -0,0 +1 @@
1
+ export function extra(dq: any): void;
@@ -0,0 +1 @@
1
+ export function shadow(dq: any, Query: any): void;
@@ -0,0 +1 @@
1
+ export { shadow } from "./plugins/shadow.js";
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@thomasseanfahey/domq",
3
+ "version": "0.1.0",
4
+ "description": "Query DOM relationships, not selectors. Composable traversal + predicates with deterministic semantics and lazy evaluation.",
5
+ "type": "module",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./src/index.js"
11
+ },
12
+ "./shadow": {
13
+ "types": "./dist/shadow.d.ts",
14
+ "default": "./src/shadow.js"
15
+ },
16
+ "./extra": {
17
+ "types": "./dist/extra.d.ts",
18
+ "default": "./src/extra.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "src",
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "sideEffects": false,
28
+ "engines": { "node": ">=18" },
29
+ "keywords": [
30
+ "dom",
31
+ "dom-traversal",
32
+ "dom-query",
33
+ "browser",
34
+ "browser-extension",
35
+ "scraping",
36
+ "testing",
37
+ "shadow-dom",
38
+ "predicate",
39
+ "query"
40
+ ],
41
+ "scripts": {
42
+ "test": "node --test",
43
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true });\"",
44
+ "build:types": "npm run clean && npx -y tsc -p tsconfig.json",
45
+ "prepack": "npm run build:types",
46
+ "prepublishOnly": "npm test && npm run build:types",
47
+ "pack:check": "npm pack --dry-run"
48
+ },
49
+ "devDependencies": { "typescript": "^5.6.3" },
50
+ "license": "MIT",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }
package/src/core/dq.js ADDED
@@ -0,0 +1,24 @@
1
+ import { Query } from './query.js';
2
+ import { and, or, not, predicate, attr, data, dataset, text, ownText, value, tag, hasClass, matches, role } from './predicates.js';
3
+ import { self, parent, ancestors, children, descendants, siblings, followingSiblings, precedingSiblings, next, prev, closest, find, within, until } from './relations.js';
4
+
5
+ function dq(node) { return Query.from(node); }
6
+ dq.all = (iterable) => Query.fromAll(iterable);
7
+
8
+ dq.compile = (builder) => {
9
+ if (typeof builder !== 'function') throw new TypeError('dq.compile(builder) requires a function');
10
+ return (start, options = undefined) => {
11
+ const q = dq(start);
12
+ if (options) { if (options.budget) q.budget(options.budget); if (options.debug) q.debug(options.debug); }
13
+ return builder(q);
14
+ };
15
+ };
16
+
17
+ dq.use = (plugin) => { if (typeof plugin !== 'function') throw new TypeError('dq.use(plugin) requires a function'); plugin(dq, Query); };
18
+
19
+ Object.assign(dq, { and, or, not, predicate, attr, data, dataset, text, ownText, value, tag, hasClass, matches, role });
20
+ Object.assign(dq, { relations: { self, parent, ancestors, children, descendants, siblings, followingSiblings, precedingSiblings, next, prev, closest, find, within, until } });
21
+
22
+ Query.installBaseRelations({ self, parent, ancestors, children, descendants, siblings, followingSiblings, precedingSiblings, next, prev, closest, find, within, until });
23
+
24
+ export { dq };
@@ -0,0 +1,67 @@
1
+ import { getOwnText, getValue, normaliseText, safeMatches } from './util.js';
2
+
3
+ function attachDescribe(fn, describe) {
4
+ Object.defineProperty(fn, '_domqDescribe', { value: () => describe, enumerable: false });
5
+ return fn;
6
+ }
7
+
8
+ export function predicate(fn, describe = 'predicate') {
9
+ if (typeof fn !== 'function') throw new TypeError('dq.predicate(fn) requires a function');
10
+ return attachDescribe((el) => Boolean(fn(el)), String(describe));
11
+ }
12
+
13
+ export function and(...preds) {
14
+ preds = preds.flat().filter(Boolean);
15
+ const desc = preds.map(p => (p && p._domqDescribe ? p._domqDescribe() : 'predicate')).join(' AND ');
16
+ return attachDescribe((el) => preds.every(p => p(el)), `(${desc})`);
17
+ }
18
+
19
+ export function or(...preds) {
20
+ preds = preds.flat().filter(Boolean);
21
+ const desc = preds.map(p => (p && p._domqDescribe ? p._domqDescribe() : 'predicate')).join(' OR ');
22
+ return attachDescribe((el) => preds.some(p => p(el)), `(${desc})`);
23
+ }
24
+
25
+ export function not(pred) {
26
+ const desc = pred && pred._domqDescribe ? pred._domqDescribe() : 'predicate';
27
+ return attachDescribe((el) => !pred(el), `NOT(${desc})`);
28
+ }
29
+
30
+ class ComparatorBuilder {
31
+ constructor(kind, getter) { this._kind = kind; this._getter = getter; }
32
+ exists() { return attachDescribe((el) => { const v = this._getter(el); return v !== null && v !== undefined && v !== ''; }, `${this._kind}.exists()`); }
33
+ eq(value) { return attachDescribe((el) => this._getter(el) === value, `${this._kind} == ${JSON.stringify(value)}`); }
34
+ ne(value) { return attachDescribe((el) => this._getter(el) !== value, `${this._kind} != ${JSON.stringify(value)}`); }
35
+ includes(substr) { const s = String(substr); return attachDescribe((el) => String(this._getter(el) ?? '').includes(s), `${this._kind}.includes(${JSON.stringify(s)})`); }
36
+ startsWith(prefix) { const p = String(prefix); return attachDescribe((el) => String(this._getter(el) ?? '').startsWith(p), `${this._kind}.startsWith(${JSON.stringify(p)})`); }
37
+ endsWith(suffix) { const s = String(suffix); return attachDescribe((el) => String(this._getter(el) ?? '').endsWith(s), `${this._kind}.endsWith(${JSON.stringify(s)})`); }
38
+ matches(re) { const rx = (re instanceof RegExp) ? re : new RegExp(String(re)); return attachDescribe((el) => rx.test(String(this._getter(el) ?? '')), `${this._kind}.matches(${rx.toString()})`); }
39
+ truthy() { return attachDescribe((el) => Boolean(this._getter(el)), `${this._kind}.truthy()`); }
40
+ gt(n) { const num = Number(n); return attachDescribe((el) => Number(this._getter(el)) > num, `${this._kind} > ${num}`); }
41
+ gte(n) { const num = Number(n); return attachDescribe((el) => Number(this._getter(el)) >= num, `${this._kind} >= ${num}`); }
42
+ lt(n) { const num = Number(n); return attachDescribe((el) => Number(this._getter(el)) < num, `${this._kind} < ${num}`); }
43
+ lte(n) { const num = Number(n); return attachDescribe((el) => Number(this._getter(el)) <= num, `${this._kind} <= ${num}`); }
44
+ }
45
+
46
+ export function attr(name) { const key = String(name); return new ComparatorBuilder(`attr(${JSON.stringify(key)})`, (el) => el.getAttribute(key)); }
47
+
48
+ export function data(key) {
49
+ const k = String(key);
50
+ return new ComparatorBuilder(`data(${JSON.stringify(k)})`, (el) => {
51
+ const ds = el.dataset || {};
52
+ if (k in ds) return ds[k];
53
+ const camel = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
54
+ if (camel in ds) return ds[camel];
55
+ return el.getAttribute(`data-${k}`);
56
+ });
57
+ }
58
+
59
+ export function dataset(key) { return data(key); }
60
+ export function text() { return new ComparatorBuilder('text()', (el) => normaliseText(el.textContent || '')); }
61
+ export function ownText() { return new ComparatorBuilder('ownText()', (el) => getOwnText(el)); }
62
+ export function value() { return new ComparatorBuilder('value()', (el) => getValue(el)); }
63
+
64
+ export function tag(tagName) { const t = String(tagName).toLowerCase(); return attachDescribe((el) => (el.tagName || '').toLowerCase() === t, `tag(${JSON.stringify(t)})`); }
65
+ export function hasClass(className) { const c = String(className); return attachDescribe((el) => el.classList ? el.classList.contains(c) : false, `hasClass(${JSON.stringify(c)})`); }
66
+ export function matches(selector) { const s = String(selector); return attachDescribe((el) => safeMatches(el, s), `matches(${JSON.stringify(s)})`); }
67
+ export function role(roleName) { const r = String(roleName); return attachDescribe((el) => el.getAttribute('role') === r, `role(${JSON.stringify(r)})`); }
@@ -0,0 +1,235 @@
1
+ import { describeElement, toIterable, toElement } from './util.js';
2
+
3
+ function nowMs() { return (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now(); }
4
+
5
+ function checkBudget(ctx) {
6
+ const b = ctx.budget;
7
+ if (!b) return;
8
+ if (typeof b.maxNodes === 'number' && ctx.visitedNodes > b.maxNodes) {
9
+ const err = new Error(`domq: traversal budget exceeded (maxNodes=${b.maxNodes})`);
10
+ err.code = 'DOMQ_BUDGET_NODES';
11
+ throw err;
12
+ }
13
+ if (typeof b.maxMs === 'number') {
14
+ const elapsed = nowMs() - ctx.startedAt;
15
+ if (elapsed > b.maxMs) {
16
+ const err = new Error(`domq: traversal budget exceeded (maxMs=${b.maxMs}, elapsed=${Math.round(elapsed)}ms)`);
17
+ err.code = 'DOMQ_BUDGET_TIME';
18
+ throw err;
19
+ }
20
+ }
21
+ }
22
+
23
+ export class Query {
24
+ constructor(iterableFactory, steps) {
25
+ this._iterableFactory = iterableFactory;
26
+ this._steps = steps || [];
27
+ this._budget = {};
28
+ this._debugEnabled = false;
29
+ this._debugLabel = null;
30
+ }
31
+
32
+ static from(node) {
33
+ const el = toElement(node);
34
+ return new Query(
35
+ (ctx) => (function* () { if (!el) return; ctx.visitedNodes += 1; checkBudget(ctx); yield el; })(),
36
+ el ? [`from(${describeElement(el)})`] : ['from(null)']
37
+ );
38
+ }
39
+
40
+ static fromAll(iterable) {
41
+ const it = toIterable(iterable);
42
+ return new Query(
43
+ (ctx) => (function* () {
44
+ for (const el of it) {
45
+ if (!(el instanceof Element)) continue;
46
+ ctx.visitedNodes += 1;
47
+ checkBudget(ctx);
48
+ yield el;
49
+ }
50
+ })(),
51
+ ['fromAll(...)']
52
+ );
53
+ }
54
+
55
+ static installBaseRelations(relations) {
56
+ for (const [name, fn] of Object.entries(relations)) {
57
+ if (Query.prototype[name]) continue;
58
+ Query.prototype[name] = function (...args) { return this.get(fn, ...args); };
59
+ }
60
+ }
61
+
62
+ get(relation, ...args) {
63
+ if (typeof relation !== 'function') throw new TypeError('domq: relation must be a function');
64
+ const prevFactory = this._iterableFactory;
65
+ const step = relation._domqName ? `${relation._domqName}(${args.map(String).join(', ')})` : 'relation(...)';
66
+ const nextFactory = (ctx) => relation(prevFactory(ctx), ctx, ...args);
67
+ return this._clone(nextFactory, [...this._steps, step]);
68
+ }
69
+
70
+ where(pred) {
71
+ if (typeof pred !== 'function') throw new TypeError('domq: predicate must be a function');
72
+ const prevFactory = this._iterableFactory;
73
+ const label = pred._domqDescribe ? pred._domqDescribe() : 'predicate';
74
+ const nextFactory = (ctx) => (function* () { for (const el of prevFactory(ctx)) if (pred(el)) yield el; })();
75
+ return this._clone(nextFactory, [...this._steps, `where(${label})`]);
76
+ }
77
+
78
+ not(pred) {
79
+ if (typeof pred !== 'function') throw new TypeError('domq: predicate must be a function');
80
+ const prevFactory = this._iterableFactory;
81
+ const label = pred._domqDescribe ? pred._domqDescribe() : 'predicate';
82
+ const nextFactory = (ctx) => (function* () { for (const el of prevFactory(ctx)) if (!pred(el)) yield el; })();
83
+ return this._clone(nextFactory, [...this._steps, `not(${label})`]);
84
+ }
85
+
86
+ unique() {
87
+ const prevFactory = this._iterableFactory;
88
+ const nextFactory = (ctx) => (function* () {
89
+ const seen = new Set();
90
+ for (const el of prevFactory(ctx)) { if (seen.has(el)) continue; seen.add(el); yield el; }
91
+ })();
92
+ return this._clone(nextFactory, [...this._steps, 'unique()']);
93
+ }
94
+
95
+ uniqueBy(keyFn) {
96
+ if (typeof keyFn !== 'function') throw new TypeError('domq: keyFn must be a function');
97
+ const prevFactory = this._iterableFactory;
98
+ const nextFactory = (ctx) => (function* () {
99
+ const seen = new Set();
100
+ for (const el of prevFactory(ctx)) { const key = keyFn(el); if (seen.has(key)) continue; seen.add(key); yield el; }
101
+ })();
102
+ return this._clone(nextFactory, [...this._steps, 'uniqueBy(fn)']);
103
+ }
104
+
105
+ reverse() {
106
+ const prevFactory = this._iterableFactory;
107
+ const nextFactory = (ctx) => { const arr = Array.from(prevFactory(ctx)); arr.reverse(); return arr; };
108
+ return this._clone(nextFactory, [...this._steps, 'reverse()']);
109
+ }
110
+
111
+ budget(budget) { this._budget = Object.assign({}, this._budget, budget || {}); return this; }
112
+ debug(label = true) { this._debugEnabled = Boolean(label); this._debugLabel = (typeof label === 'string') ? label : null; return this; }
113
+ explain() { return this._steps.join(' -> '); }
114
+
115
+ first() {
116
+ const { ctx, iterable } = this._evaluate();
117
+ let count = 0;
118
+ for (const el of iterable) { count += 1; this._maybeDebug(ctx, count); return el; }
119
+ this._maybeDebug(ctx, 0);
120
+ return null;
121
+ }
122
+
123
+ exists() {
124
+ const { ctx, iterable } = this._evaluate();
125
+ for (const _ of iterable) { this._maybeDebug(ctx, 1); return true; }
126
+ this._maybeDebug(ctx, 0);
127
+ return false;
128
+ }
129
+
130
+ one() {
131
+ const { ctx, iterable } = this._evaluate();
132
+ const matches = [];
133
+ for (const el of iterable) { matches.push(el); if (matches.length > 2) break; }
134
+ this._maybeDebug(ctx, matches.length);
135
+ if (matches.length === 1) return matches[0];
136
+ const summary = matches.map(describeElement).join(', ');
137
+ const err = new Error(`domq.one(): expected exactly 1 match, got ${matches.length}.\nPipeline: ${this.explain()}\n` + (matches.length ? `Sample: ${summary}` : 'No matches.'));
138
+ err.code = 'DOMQ_ONE';
139
+ throw err;
140
+ }
141
+
142
+ maybeOne() {
143
+ const { ctx, iterable } = this._evaluate();
144
+ let first = null;
145
+ let count = 0;
146
+ for (const el of iterable) { count += 1; if (count === 1) first = el; if (count > 1) break; }
147
+ this._maybeDebug(ctx, count);
148
+ if (count <= 1) return first;
149
+ const err = new Error(`domq.maybeOne(): expected 0 or 1 match, got ${count}.\nPipeline: ${this.explain()}`);
150
+ err.code = 'DOMQ_MAYBEONE';
151
+ throw err;
152
+ }
153
+
154
+ at(n) {
155
+ if (!Number.isInteger(n)) throw new TypeError('domq.at(n) requires an integer');
156
+ const prevFactory = this._iterableFactory;
157
+ const nextFactory = (ctx) => (function* () {
158
+ const arr = Array.from(prevFactory(ctx));
159
+ const idx = n < 0 ? arr.length + n : n;
160
+ if (idx < 0 || idx >= arr.length) return;
161
+ yield arr[idx];
162
+ })();
163
+ return this._clone(nextFactory, [...this._steps, `at(${n})`]);
164
+ }
165
+
166
+ take(n) {
167
+ if (!Number.isInteger(n) || n < 0) throw new TypeError('domq.take(n) requires n >= 0 integer');
168
+ const prevFactory = this._iterableFactory;
169
+ const nextFactory = (ctx) => (function* () { let i = 0; for (const el of prevFactory(ctx)) { if (i++ >= n) break; yield el; } })();
170
+ return this._clone(nextFactory, [...this._steps, `take(${n})`]);
171
+ }
172
+
173
+ skip(n) {
174
+ if (!Number.isInteger(n) || n < 0) throw new TypeError('domq.skip(n) requires n >= 0 integer');
175
+ const prevFactory = this._iterableFactory;
176
+ const nextFactory = (ctx) => (function* () { let i = 0; for (const el of prevFactory(ctx)) { if (i++ < n) continue; yield el; } })();
177
+ return this._clone(nextFactory, [...this._steps, `skip(${n})`]);
178
+ }
179
+
180
+ slice(start, end) {
181
+ if (!Number.isInteger(start)) throw new TypeError('domq.slice(start, end) requires integer start');
182
+ if (end != null && !Number.isInteger(end)) throw new TypeError('domq.slice(start, end) requires integer end or null');
183
+ const prevFactory = this._iterableFactory;
184
+ const nextFactory = (ctx) => (function* () { const arr = Array.from(prevFactory(ctx)); for (const el of arr.slice(start, end == null ? undefined : end)) yield el; })();
185
+ return this._clone(nextFactory, [...this._steps, `slice(${start}, ${end == null ? 'null' : end})`]);
186
+ }
187
+
188
+ count() {
189
+ const { ctx, iterable } = this._evaluate();
190
+ let c = 0;
191
+ for (const _ of iterable) c += 1;
192
+ this._maybeDebug(ctx, c);
193
+ return c;
194
+ }
195
+
196
+ toArray() {
197
+ const { ctx, iterable } = this._evaluate();
198
+ const arr = Array.from(iterable);
199
+ this._maybeDebug(ctx, arr.length);
200
+ return arr;
201
+ }
202
+
203
+ map(fn) {
204
+ if (typeof fn !== 'function') throw new TypeError('domq.map(fn) requires a function');
205
+ const { ctx, iterable } = this._evaluate();
206
+ const out = [];
207
+ let i = 0;
208
+ for (const el of iterable) out.push(fn(el, i++));
209
+ this._maybeDebug(ctx, i);
210
+ return out;
211
+ }
212
+
213
+ _clone(iterableFactory, steps) {
214
+ const q = new Query(iterableFactory, steps);
215
+ q._budget = Object.assign({}, this._budget);
216
+ q._debugEnabled = this._debugEnabled;
217
+ q._debugLabel = this._debugLabel;
218
+ return q;
219
+ }
220
+
221
+ _evaluate() {
222
+ const ctx = { budget: Object.assign({}, this._budget), debugEnabled: this._debugEnabled, debugLabel: this._debugLabel, visitedNodes: 0, startedAt: nowMs() };
223
+ const iterable = (function* (factory, ctx) { for (const el of factory(ctx)) { ctx.visitedNodes += 1; checkBudget(ctx); yield el; } })(this._iterableFactory, ctx);
224
+ return { ctx, iterable };
225
+ }
226
+
227
+ _maybeDebug(ctx, matchCount) {
228
+ if (!ctx.debugEnabled) return;
229
+ const label = ctx.debugLabel ? ` ${ctx.debugLabel}` : '';
230
+ const elapsed = Math.round((nowMs() - ctx.startedAt) * 100) / 100;
231
+ console.log(`[domq${label}] matches=${matchCount} visited=${ctx.visitedNodes} time=${elapsed}ms\n${this.explain()}`);
232
+ }
233
+ }
234
+
235
+ export { checkBudget };
@@ -0,0 +1,106 @@
1
+ import { safeMatches } from './util.js';
2
+
3
+ function named(name, fn) { Object.defineProperty(fn, '_domqName', { value: name, enumerable: false }); return fn; }
4
+ function maxDepth(ctx) { return (ctx && ctx.budget && typeof ctx.budget.maxDepth === 'number') ? ctx.budget.maxDepth : null; }
5
+
6
+ export const self = named('self', (nodes) => nodes);
7
+
8
+ export const parent = named('parent', (nodes) => (function* () { for (const el of nodes) if (el.parentElement) yield el.parentElement; })());
9
+
10
+ export const ancestors = named('ancestors', (nodes, ctx) => {
11
+ const limit = maxDepth(ctx);
12
+ return (function* () {
13
+ for (const el of nodes) {
14
+ let cur = el.parentElement;
15
+ let depth = 0;
16
+ while (cur) {
17
+ if (limit != null && depth >= limit) break;
18
+ yield cur;
19
+ cur = cur.parentElement;
20
+ depth += 1;
21
+ }
22
+ }
23
+ })();
24
+ });
25
+
26
+ export const children = named('children', (nodes) => (function* () { for (const el of nodes) for (const c of el.children) yield c; })());
27
+
28
+ export const descendants = named('descendants', (nodes, ctx) => {
29
+ const limit = maxDepth(ctx);
30
+ return (function* () {
31
+ for (const root of nodes) {
32
+ const stack = [];
33
+ const kids = root.children;
34
+ for (let i = kids.length - 1; i >= 0; i -= 1) stack.push({ el: kids[i], depth: 1 });
35
+ while (stack.length) {
36
+ const { el, depth } = stack.pop();
37
+ if (limit != null && depth > limit) continue;
38
+ yield el;
39
+ const ck = el.children;
40
+ for (let i = ck.length - 1; i >= 0; i -= 1) stack.push({ el: ck[i], depth: depth + 1 });
41
+ }
42
+ }
43
+ })();
44
+ });
45
+
46
+ export const siblings = named('siblings', (nodes) => (function* () {
47
+ for (const el of nodes) {
48
+ const p = el.parentElement;
49
+ if (!p) continue;
50
+ for (const c of p.children) if (c !== el) yield c;
51
+ }
52
+ })());
53
+
54
+ export const followingSiblings = named('followingSiblings', (nodes) => (function* () {
55
+ for (const el of nodes) { let cur = el.nextElementSibling; while (cur) { yield cur; cur = cur.nextElementSibling; } }
56
+ })());
57
+
58
+ export const precedingSiblings = named('precedingSiblings', (nodes) => (function* () {
59
+ for (const el of nodes) {
60
+ const acc = [];
61
+ let cur = el.previousElementSibling;
62
+ while (cur) { acc.push(cur); cur = cur.previousElementSibling; }
63
+ acc.reverse();
64
+ for (const x of acc) yield x;
65
+ }
66
+ })());
67
+
68
+ export const next = named('next', (nodes) => (function* () { for (const el of nodes) if (el.nextElementSibling) yield el.nextElementSibling; })());
69
+ export const prev = named('prev', (nodes) => (function* () { for (const el of nodes) if (el.previousElementSibling) yield el.previousElementSibling; })());
70
+
71
+ export const closest = named('closest', (nodes, _ctx, selector) => {
72
+ const s = String(selector);
73
+ return (function* () { for (const el of nodes) { const c = el.closest(s); if (c) yield c; } })();
74
+ });
75
+
76
+ export const find = named('find', (nodes, _ctx, selector) => {
77
+ const s = String(selector);
78
+ return (function* () {
79
+ for (const el of nodes) {
80
+ try { for (const x of el.querySelectorAll(s)) yield x; } catch {}
81
+ }
82
+ })();
83
+ });
84
+
85
+ export const within = named('within', (nodes, _ctx, selector) => {
86
+ const s = String(selector);
87
+ return (function* () { for (const el of nodes) { const c = el.closest(s); if (c) yield c; } })();
88
+ });
89
+
90
+ export const until = named('until', (nodes, _ctx, boundary) => {
91
+ const boundaryPred = (typeof boundary === 'string') ? (el) => safeMatches(el, boundary)
92
+ : (typeof boundary === 'function') ? boundary : null;
93
+ if (!boundaryPred) throw new TypeError('domq.until(boundary) requires a selector string or predicate function');
94
+
95
+ return (function* () {
96
+ for (const el of nodes) {
97
+ let cur = el;
98
+ while (cur && cur.parentElement) {
99
+ cur = cur.parentElement;
100
+ if (!cur) break;
101
+ if (boundaryPred(cur)) break;
102
+ yield cur;
103
+ }
104
+ }
105
+ })();
106
+ });
@@ -0,0 +1,54 @@
1
+ export function toElement(node) {
2
+ if (!node) return null;
3
+ if (node instanceof Element) return node;
4
+ if (node && typeof node === 'object' && 'nodeType' in node) {
5
+ if (node.nodeType === 1) return node;
6
+ if (node.parentElement instanceof Element) return node.parentElement;
7
+ }
8
+ return null;
9
+ }
10
+
11
+ export function toIterable(iterable) {
12
+ if (!iterable) return [];
13
+ if (typeof iterable[Symbol.iterator] === 'function') return iterable;
14
+ if (typeof iterable.length === 'number') {
15
+ return (function* () { for (let i = 0; i < iterable.length; i += 1) yield iterable[i]; })();
16
+ }
17
+ return [];
18
+ }
19
+
20
+ export function describeElement(el) {
21
+ if (!(el instanceof Element)) return String(el);
22
+ const tag = el.tagName ? el.tagName.toLowerCase() : 'element';
23
+ const id = el.id ? `#${el.id}` : '';
24
+ let cls = '';
25
+ if (typeof el.className === 'string' && el.className.trim()) {
26
+ const parts = el.className.trim().split(/\s+/).slice(0, 3);
27
+ cls = parts.length ? `.${parts.join('.')}` : '';
28
+ }
29
+ const attrs = [];
30
+ const keys = ['data-testid', 'data-test', 'data-qa', 'name', 'role', 'aria-label'];
31
+ for (const k of keys) {
32
+ const v = el.getAttribute && el.getAttribute(k);
33
+ if (v) attrs.push(`${k}=\"${v}\"`);
34
+ if (attrs.length >= 2) break;
35
+ }
36
+ const attrStr = attrs.length ? `[${attrs.join(' ')}]` : '';
37
+ return `${tag}${id}${cls}${attrStr}`;
38
+ }
39
+
40
+ export function normaliseText(s) { return String(s ?? '').replace(/\s+/g, ' ').trim(); }
41
+
42
+ export function getOwnText(el) {
43
+ let out = '';
44
+ for (const n of el.childNodes) if (n.nodeType === 3) out += n.nodeValue || '';
45
+ return normaliseText(out);
46
+ }
47
+
48
+ export function getValue(el) {
49
+ const tag = el.tagName ? el.tagName.toLowerCase() : '';
50
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') return String(el.value ?? '');
51
+ return '';
52
+ }
53
+
54
+ export function safeMatches(el, selector) { try { return el.matches(selector); } catch { return false; } }
package/src/extra.js ADDED
@@ -0,0 +1 @@
1
+ export { extra } from './plugins/extra.js';
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { dq } from './core/dq.js';
2
+ export { shadow } from './plugins/shadow.js';
3
+ export { extra } from './plugins/extra.js';
@@ -0,0 +1,58 @@
1
+ function attachDescribe(fn, desc) { Object.defineProperty(fn, '_domqDescribe', { value: () => desc, enumerable: false }); return fn; }
2
+ function getComputed(el) { try { return window.getComputedStyle(el); } catch { return null; } }
3
+
4
+ function visiblePredicate() {
5
+ return attachDescribe((el) => {
6
+ if (!(el instanceof Element)) return false;
7
+ const cs = getComputed(el);
8
+ if (!cs) return false;
9
+ if (cs.display === 'none') return false;
10
+ if (cs.visibility === 'hidden') return false;
11
+ if (Number(cs.opacity) === 0) return false;
12
+ const rects = el.getClientRects();
13
+ return rects && rects.length > 0;
14
+ }, 'visible()');
15
+ }
16
+
17
+ function inViewportPredicate(options = {}) {
18
+ const threshold = typeof options.threshold === 'number' ? options.threshold : 0;
19
+ return attachDescribe((el) => {
20
+ if (!(el instanceof Element)) return false;
21
+ const r = el.getBoundingClientRect();
22
+ const vw = window.innerWidth || document.documentElement.clientWidth || 0;
23
+ const vh = window.innerHeight || document.documentElement.clientHeight || 0;
24
+ if (vw <= 0 || vh <= 0) return false;
25
+
26
+ const interLeft = Math.max(0, Math.min(vw, r.right) - Math.max(0, r.left));
27
+ const interTop = Math.max(0, Math.min(vh, r.bottom) - Math.max(0, r.top));
28
+ const interArea = interLeft * interTop;
29
+ const area = Math.max(0, r.width) * Math.max(0, r.height);
30
+ if (area === 0) return false;
31
+
32
+ return (interArea / area) >= threshold;
33
+ }, `inViewport(${JSON.stringify({ threshold })})`);
34
+ }
35
+
36
+ function styleFactory(prop) {
37
+ const p = String(prop);
38
+ return {
39
+ eq(value) { return attachDescribe((el) => { const cs = getComputed(el); return cs ? cs.getPropertyValue(p) === String(value) : false; }, `style(${JSON.stringify(p)}) == ${JSON.stringify(String(value))}`); },
40
+ ne(value) { return attachDescribe((el) => { const cs = getComputed(el); return cs ? cs.getPropertyValue(p) !== String(value) : false; }, `style(${JSON.stringify(p)}) != ${JSON.stringify(String(value))}`); },
41
+ matches(re) { const rx = re instanceof RegExp ? re : new RegExp(String(re)); return attachDescribe((el) => { const cs = getComputed(el); const v = cs ? cs.getPropertyValue(p) : ''; return rx.test(v); }, `style(${JSON.stringify(p)}).matches(${rx.toString()})`); },
42
+ includes(substr) { const s = String(substr); return attachDescribe((el) => { const cs = getComputed(el); const v = cs ? cs.getPropertyValue(p) : ''; return v.includes(s); }, `style(${JSON.stringify(p)}).includes(${JSON.stringify(s)})`); }
43
+ };
44
+ }
45
+
46
+ function rectFactory() {
47
+ return {
48
+ width: { gt(n) { const num = Number(n); return attachDescribe((el) => el.getBoundingClientRect().width > num, `rect().width > ${num}`); }, lt(n) { const num = Number(n); return attachDescribe((el) => el.getBoundingClientRect().width < num, `rect().width < ${num}`); } },
49
+ height: { gt(n) { const num = Number(n); return attachDescribe((el) => el.getBoundingClientRect().height > num, `rect().height > ${num}`); }, lt(n) { const num = Number(n); return attachDescribe((el) => el.getBoundingClientRect().height < num, `rect().height < ${num}`); } }
50
+ };
51
+ }
52
+
53
+ export function extra(dq) {
54
+ dq.visible = visiblePredicate;
55
+ dq.inViewport = inViewportPredicate;
56
+ dq.style = styleFactory;
57
+ dq.rect = rectFactory;
58
+ }
@@ -0,0 +1,80 @@
1
+ function named(name, fn) { Object.defineProperty(fn, '_domqName', { value: name, enumerable: false }); return fn; }
2
+
3
+ function composedParent(el) {
4
+ if (el.assignedSlot) return el.assignedSlot;
5
+ const root = el.getRootNode && el.getRootNode();
6
+ if (root && root.host instanceof Element) return root.host;
7
+ return el.parentElement;
8
+ }
9
+
10
+ const shadowRootRel = named('shadowRoot', (nodes) => (function* () {
11
+ for (const el of nodes) {
12
+ const sr = el.shadowRoot;
13
+ if (sr && sr instanceof ShadowRoot) for (const child of sr.children) yield child;
14
+ }
15
+ })());
16
+
17
+ const composedAncestorsRel = named('composedAncestors', (nodes, ctx) => {
18
+ const limit = (ctx && ctx.budget && typeof ctx.budget.maxDepth === 'number') ? ctx.budget.maxDepth : null;
19
+ return (function* () {
20
+ for (const el of nodes) {
21
+ let cur = el;
22
+ let depth = 0;
23
+ while (cur) {
24
+ const p = composedParent(cur);
25
+ if (!p) break;
26
+ if (limit != null && depth >= limit) break;
27
+ yield p;
28
+ cur = p;
29
+ depth += 1;
30
+ }
31
+ }
32
+ })();
33
+ });
34
+
35
+ const composedDescendantsRel = named('composedDescendants', (nodes, ctx) => {
36
+ const limit = (ctx && ctx.budget && typeof ctx.budget.maxDepth === 'number') ? ctx.budget.maxDepth : null;
37
+
38
+ function* iterChildren(el) {
39
+ for (const c of el.children) yield c;
40
+ if (el.shadowRoot) for (const c of el.shadowRoot.children) yield c;
41
+ }
42
+
43
+ return (function* () {
44
+ for (const root of nodes) {
45
+ const stack = [];
46
+ const kids = Array.from(iterChildren(root));
47
+ for (let i = kids.length - 1; i >= 0; i -= 1) stack.push({ el: kids[i], depth: 1 });
48
+ while (stack.length) {
49
+ const { el, depth } = stack.pop();
50
+ if (limit != null && depth > limit) continue;
51
+ yield el;
52
+ const ck = Array.from(iterChildren(el));
53
+ for (let i = ck.length - 1; i >= 0; i -= 1) stack.push({ el: ck[i], depth: depth + 1 });
54
+ }
55
+ }
56
+ })();
57
+ });
58
+
59
+ const assignedSlotRel = named('assignedSlot', (nodes) => (function* () { for (const el of nodes) if (el.assignedSlot) yield el.assignedSlot; })());
60
+ const assignedElementsRel = named('assignedElements', (nodes) => (function* () {
61
+ for (const el of nodes) {
62
+ if (el instanceof HTMLSlotElement) {
63
+ const assigned = el.assignedElements ? el.assignedElements({ flatten: true }) : [];
64
+ for (const a of assigned) yield a;
65
+ }
66
+ }
67
+ })());
68
+
69
+ export function shadow(dq, Query) {
70
+ dq.relations.shadowRoot = shadowRootRel;
71
+ dq.relations.composedAncestors = composedAncestorsRel;
72
+ dq.relations.composedDescendants = composedDescendantsRel;
73
+ dq.relations.assignedSlot = assignedSlotRel;
74
+ dq.relations.assignedElements = assignedElementsRel;
75
+
76
+ const methods = { shadowRoot: shadowRootRel, composedAncestors: composedAncestorsRel, composedDescendants: composedDescendantsRel, assignedSlot: assignedSlotRel, assignedElements: assignedElementsRel };
77
+ for (const [name, fn] of Object.entries(methods)) {
78
+ if (!Query.prototype[name]) Query.prototype[name] = function (...args) { return this.get(fn, ...args); };
79
+ }
80
+ }
package/src/shadow.js ADDED
@@ -0,0 +1 @@
1
+ export { shadow } from './plugins/shadow.js';