@sable-ai/sdk-core 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/README.md +55 -0
- package/dist/esm/index.js +2431 -0
- package/dist/sable.iife.js +1486 -0
- package/dist/types/browser-bridge/actions.d.ts +27 -0
- package/dist/types/browser-bridge/dom-state.d.ts +37 -0
- package/dist/types/browser-bridge/index.d.ts +19 -0
- package/dist/types/connection/index.d.ts +26 -0
- package/dist/types/events/index.d.ts +15 -0
- package/dist/types/global.d.ts +26 -0
- package/dist/types/index.d.ts +23 -0
- package/dist/types/rpc.d.ts +22 -0
- package/dist/types/runtime/clipboard.d.ts +14 -0
- package/dist/types/runtime/index.d.ts +36 -0
- package/dist/types/runtime/video-overlay.d.ts +14 -0
- package/dist/types/session/debug-panel.d.ts +29 -0
- package/dist/types/session/index.d.ts +41 -0
- package/dist/types/types/index.d.ts +131 -0
- package/dist/types/version.d.ts +7 -0
- package/dist/types/vision/frame-source.d.ts +34 -0
- package/dist/types/vision/index.d.ts +29 -0
- package/dist/types/vision/publisher.d.ts +44 -0
- package/dist/types/vision/wireframe.d.ts +22 -0
- package/package.json +61 -0
- package/src/assets/visible-dom.js.txt +764 -0
- package/src/assets/wireframe.js.txt +678 -0
- package/src/assets.d.ts +24 -0
- package/src/browser-bridge/actions.ts +161 -0
- package/src/browser-bridge/dom-state.ts +103 -0
- package/src/browser-bridge/index.ts +99 -0
- package/src/connection/index.ts +49 -0
- package/src/events/index.ts +50 -0
- package/src/global.ts +35 -0
- package/src/index.test.ts +6 -0
- package/src/index.ts +43 -0
- package/src/rpc.ts +31 -0
- package/src/runtime/clipboard.ts +47 -0
- package/src/runtime/index.ts +138 -0
- package/src/runtime/video-overlay.ts +94 -0
- package/src/session/debug-panel.ts +254 -0
- package/src/session/index.ts +375 -0
- package/src/types/index.ts +176 -0
- package/src/version.ts +8 -0
- package/src/vision/frame-source.ts +111 -0
- package/src/vision/index.ts +70 -0
- package/src/vision/publisher.ts +106 -0
- package/src/vision/wireframe.ts +43 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
() => {
|
|
2
|
+
const vw = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
3
|
+
const vh = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
4
|
+
const sx = window.scrollX || window.pageXOffset || 0;
|
|
5
|
+
const sy = window.scrollY || window.pageYOffset || 0;
|
|
6
|
+
|
|
7
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
8
|
+
const norm = (s) => (s || "").trim().replace(/\s+/g, " ");
|
|
9
|
+
const SKIP_TAGS = new Set(["script","style","noscript","template"]);
|
|
10
|
+
const WRAPPER_TAGS = new Set(["div","span","section","article","main","nav","header","footer"]);
|
|
11
|
+
|
|
12
|
+
const styleCache = new WeakMap();
|
|
13
|
+
const getStyleBits = (el) => {
|
|
14
|
+
const cached = styleCache.get(el);
|
|
15
|
+
if (cached) return cached;
|
|
16
|
+
let cs;
|
|
17
|
+
try { cs = window.getComputedStyle(el); } catch { cs = null; }
|
|
18
|
+
const bits = cs ? {
|
|
19
|
+
display: cs.display || "",
|
|
20
|
+
visibility: cs.visibility || "",
|
|
21
|
+
opacity: cs.opacity || "1",
|
|
22
|
+
} : { display: "", visibility: "", opacity: "1" };
|
|
23
|
+
styleCache.set(el, bits);
|
|
24
|
+
return bits;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Cache for getBoundingClientRect - avoids redundant layout calls
|
|
28
|
+
const rectCache = new WeakMap();
|
|
29
|
+
const getCachedRect = (el) => {
|
|
30
|
+
const cached = rectCache.get(el);
|
|
31
|
+
if (cached !== undefined) return cached;
|
|
32
|
+
let r;
|
|
33
|
+
try { r = el.getBoundingClientRect(); } catch { r = null; }
|
|
34
|
+
rectCache.set(el, r);
|
|
35
|
+
return r;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Cache for isControlLike - called multiple times per element
|
|
39
|
+
const controlCache = new WeakMap();
|
|
40
|
+
|
|
41
|
+
const isSkippableTag = (el) => {
|
|
42
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
43
|
+
return !tag || SKIP_TAGS.has(tag);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const intersectViewport = (r) => {
|
|
47
|
+
const left = clamp(r.left, 0, vw);
|
|
48
|
+
const right = clamp(r.right, 0, vw);
|
|
49
|
+
const top = clamp(r.top, 0, vh);
|
|
50
|
+
const bottom = clamp(r.bottom, 0, vh);
|
|
51
|
+
const w = Math.max(0, right - left);
|
|
52
|
+
const h = Math.max(0, bottom - top);
|
|
53
|
+
return { x: left, y: top, width: w, height: h };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const roleOf = (el) => el.getAttribute?.("role") || null;
|
|
57
|
+
|
|
58
|
+
const directText = (el) => {
|
|
59
|
+
try {
|
|
60
|
+
let out = "";
|
|
61
|
+
for (const n of el.childNodes || []) {
|
|
62
|
+
if (n && n.nodeType === 3) {
|
|
63
|
+
const t = norm(n.textContent);
|
|
64
|
+
if (t) out += (out ? " " : "") + t;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
} catch { return ""; }
|
|
69
|
+
};
|
|
70
|
+
const descendantText = (el, maxLen = 80) => {
|
|
71
|
+
try {
|
|
72
|
+
let t = norm(el.innerText || el.textContent || "");
|
|
73
|
+
if (!t) return "";
|
|
74
|
+
if (t.length > maxLen) t = t.slice(0, maxLen) + "…";
|
|
75
|
+
return t;
|
|
76
|
+
} catch { return ""; }
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const isMedia = (el) => {
|
|
80
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
81
|
+
return tag === "svg" || tag === "img" || tag === "canvas" || tag === "video";
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const isControlLike = (el) => {
|
|
85
|
+
const cached = controlCache.get(el);
|
|
86
|
+
if (cached !== undefined) return cached;
|
|
87
|
+
|
|
88
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
89
|
+
if (tag === "input" || tag === "textarea" || tag === "select" || tag === "button") {
|
|
90
|
+
controlCache.set(el, true); return true;
|
|
91
|
+
}
|
|
92
|
+
if (tag === "a") {
|
|
93
|
+
// treat as interactive even if Gatsby/JS navigation omits href
|
|
94
|
+
const href = (el.getAttribute("href") || "").trim();
|
|
95
|
+
if (href) { controlCache.set(el, true); return true; }
|
|
96
|
+
const role = (roleOf(el) || "").toLowerCase();
|
|
97
|
+
if (role === "link" || role === "button") { controlCache.set(el, true); return true; }
|
|
98
|
+
}
|
|
99
|
+
if (el.isContentEditable) { controlCache.set(el, true); return true; }
|
|
100
|
+
|
|
101
|
+
const role = roleOf(el);
|
|
102
|
+
if (role) {
|
|
103
|
+
const r = role.toLowerCase();
|
|
104
|
+
if ([
|
|
105
|
+
"textbox","searchbox","combobox","listbox","option",
|
|
106
|
+
"button","link","checkbox","radio","switch",
|
|
107
|
+
"tab","menuitem","slider","spinbutton"
|
|
108
|
+
].includes(r)) { controlCache.set(el, true); return true; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const tabindex = el.getAttribute?.("tabindex");
|
|
112
|
+
if (tabindex !== null && tabindex !== "-1") { controlCache.set(el, true); return true; }
|
|
113
|
+
if (el.hasAttribute?.("onclick")) { controlCache.set(el, true); return true; }
|
|
114
|
+
if (typeof el.onclick === "function") { controlCache.set(el, true); return true; }
|
|
115
|
+
|
|
116
|
+
// common misuse: clickable divs/spans with pointer cursor
|
|
117
|
+
try {
|
|
118
|
+
const cs = window.getComputedStyle(el);
|
|
119
|
+
if (cs && cs.cursor === "pointer") { controlCache.set(el, true); return true; }
|
|
120
|
+
} catch {}
|
|
121
|
+
controlCache.set(el, false);
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const hasOwnLabeling = (el) => {
|
|
126
|
+
if (directText(el)) return true;
|
|
127
|
+
const a = norm(el.getAttribute?.("aria-label"));
|
|
128
|
+
const t = norm(el.getAttribute?.("title"));
|
|
129
|
+
const alt = norm(el.getAttribute?.("alt"));
|
|
130
|
+
const ph = norm(el.getAttribute?.("placeholder"));
|
|
131
|
+
if (a || t || alt || ph) return true;
|
|
132
|
+
const cls = norm(el.getAttribute?.("class"));
|
|
133
|
+
if (cls) return true;
|
|
134
|
+
return false;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const isImportant = (el) => {
|
|
138
|
+
if (isControlLike(el)) return true;
|
|
139
|
+
if (isMedia(el)) return true;
|
|
140
|
+
if (roleOf(el)) return true;
|
|
141
|
+
if (hasOwnLabeling(el)) return true;
|
|
142
|
+
return false;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const cssEscape = (s) => {
|
|
146
|
+
if (window.CSS && typeof window.CSS.escape === "function") return window.CSS.escape(s);
|
|
147
|
+
return s.replace(/[^a-zA-Z0-9_\-]/g, (c) => "\\" + c);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const domPath = (el) => {
|
|
151
|
+
const parts = [];
|
|
152
|
+
let cur = el;
|
|
153
|
+
while (cur && cur.nodeType === 1) {
|
|
154
|
+
const tag = cur.tagName.toLowerCase();
|
|
155
|
+
const id = cur.getAttribute?.("id");
|
|
156
|
+
// IDs are unique by HTML spec; skip querySelectorAll validation for speed
|
|
157
|
+
if (id) { parts.push("#" + cssEscape(id)); break; }
|
|
158
|
+
let nth = 1;
|
|
159
|
+
let sib = cur;
|
|
160
|
+
while ((sib = sib.previousElementSibling)) {
|
|
161
|
+
if (sib.tagName.toLowerCase() === tag) nth++;
|
|
162
|
+
}
|
|
163
|
+
parts.push(`${tag}:nth-of-type(${nth})`);
|
|
164
|
+
cur = cur.parentElement;
|
|
165
|
+
if (parts.length >= 12) break;
|
|
166
|
+
}
|
|
167
|
+
return parts.reverse().join(" > ");
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const attrsOf = (el) => ({
|
|
171
|
+
"id": el.getAttribute?.("id") || null,
|
|
172
|
+
"class": el.getAttribute?.("class") || null,
|
|
173
|
+
"href": el.getAttribute?.("href") || null,
|
|
174
|
+
"type": el.getAttribute?.("type") || null,
|
|
175
|
+
"name": el.getAttribute?.("name") || null,
|
|
176
|
+
"placeholder": el.getAttribute?.("placeholder") || null,
|
|
177
|
+
"aria-label": el.getAttribute?.("aria-label") || null,
|
|
178
|
+
"aria-labelledby": el.getAttribute?.("aria-labelledby") || null,
|
|
179
|
+
"aria-expanded": el.getAttribute?.("aria-expanded") || null,
|
|
180
|
+
"aria-haspopup": el.getAttribute?.("aria-haspopup") || null,
|
|
181
|
+
"title": el.getAttribute?.("title") || null,
|
|
182
|
+
"alt": el.getAttribute?.("alt") || null,
|
|
183
|
+
"tabindex": el.getAttribute?.("tabindex") || null,
|
|
184
|
+
"contenteditable": el.isContentEditable ? "true" : (el.getAttribute?.("contenteditable") || null),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const svgDescriptor = (svg) => {
|
|
188
|
+
try {
|
|
189
|
+
if (!svg || (svg.tagName || "").toLowerCase() !== "svg") return "";
|
|
190
|
+
const viewBox = norm(svg.getAttribute("viewBox"));
|
|
191
|
+
const w = norm(svg.getAttribute("width"));
|
|
192
|
+
const h = norm(svg.getAttribute("height"));
|
|
193
|
+
const fill = norm(svg.getAttribute("fill"));
|
|
194
|
+
const stroke = norm(svg.getAttribute("stroke"));
|
|
195
|
+
const sw = norm(svg.getAttribute("stroke-width"));
|
|
196
|
+
|
|
197
|
+
const title = norm(svg.querySelector?.("title")?.textContent);
|
|
198
|
+
|
|
199
|
+
const cls = norm(svg.getAttribute("class"));
|
|
200
|
+
const useHref = norm(svg.querySelector?.("use")?.getAttribute("href") || svg.querySelector?.("use")?.getAttribute("xlink:href"));
|
|
201
|
+
|
|
202
|
+
const paths = svg.querySelectorAll?.("path") || [];
|
|
203
|
+
const pathCount = paths.length;
|
|
204
|
+
|
|
205
|
+
let dHash = "";
|
|
206
|
+
if (pathCount > 0) {
|
|
207
|
+
const d = paths[0].getAttribute("d") || "";
|
|
208
|
+
let hsh = 0;
|
|
209
|
+
for (let i = 0; i < d.length; i++) hsh = ((hsh << 5) - hsh + d.charCodeAt(i)) | 0;
|
|
210
|
+
dHash = String(hsh);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const parts = [];
|
|
214
|
+
if (title) parts.push(`title=${title}`);
|
|
215
|
+
if (useHref) parts.push(`use=${useHref}`);
|
|
216
|
+
if (viewBox) parts.push(`vb=${viewBox}`);
|
|
217
|
+
if (w || h) parts.push(`wh=${w||"?"}x${h||"?"}`);
|
|
218
|
+
if (stroke || sw) parts.push(`stroke=${stroke||""}:${sw||""}`);
|
|
219
|
+
if (fill) parts.push(`fill=${fill}`);
|
|
220
|
+
if (cls) parts.push(`cls=${cls.split(/\s+/).slice(0,3).join(".")}`);
|
|
221
|
+
if (pathCount) parts.push(`paths=${pathCount}`);
|
|
222
|
+
if (dHash) parts.push(`dhash=${dHash}`);
|
|
223
|
+
return parts.join(" ");
|
|
224
|
+
} catch { return ""; }
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const iconDescriptorFromDescendants = (el, maxSvgs = 2) => {
|
|
228
|
+
try {
|
|
229
|
+
const out = [];
|
|
230
|
+
const svgs = el.querySelectorAll?.("svg") || [];
|
|
231
|
+
for (const svg of svgs) {
|
|
232
|
+
const d = svgDescriptor(svg);
|
|
233
|
+
if (d) out.push(d);
|
|
234
|
+
if (out.length >= maxSvgs) break;
|
|
235
|
+
}
|
|
236
|
+
const imgs = el.querySelectorAll?.("img") || [];
|
|
237
|
+
for (const img of imgs) {
|
|
238
|
+
const alt = norm(img.getAttribute("alt"));
|
|
239
|
+
const title = norm(img.getAttribute("title"));
|
|
240
|
+
const cls = norm(img.getAttribute("class"));
|
|
241
|
+
const parts = [];
|
|
242
|
+
if (alt) parts.push(`alt=${alt}`);
|
|
243
|
+
if (title) parts.push(`title=${title}`);
|
|
244
|
+
if (cls) parts.push(`cls=${cls.split(/\s+/).slice(0,3).join(".")}`);
|
|
245
|
+
const d = parts.join(" ");
|
|
246
|
+
if (d) out.push("img:" + d);
|
|
247
|
+
if (out.length >= maxSvgs + 1) break;
|
|
248
|
+
}
|
|
249
|
+
return out.join(" | ");
|
|
250
|
+
} catch { return ""; }
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const baseLabelTextOf = (el) => {
|
|
254
|
+
const chunks = [];
|
|
255
|
+
const own = directText(el) || (isControlLike(el) ? descendantText(el) : "");
|
|
256
|
+
if (own) chunks.push(own);
|
|
257
|
+
|
|
258
|
+
for (const k of ["aria-label","title","alt","placeholder"]) {
|
|
259
|
+
const v = norm(el.getAttribute?.(k));
|
|
260
|
+
if (v) chunks.push(`${k}=${v}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const cls = norm(el.getAttribute?.("class"));
|
|
264
|
+
if (cls) chunks.push(`class=${cls.split(/\s+/).slice(0,3).join(" ")}`);
|
|
265
|
+
|
|
266
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
267
|
+
if (tag === "svg") {
|
|
268
|
+
const sd = svgDescriptor(el);
|
|
269
|
+
if (sd) chunks.push(sd);
|
|
270
|
+
}
|
|
271
|
+
return chunks.join(" | ");
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const needsIconScan = (el, baseName) => {
|
|
275
|
+
if (!isControlLike(el)) return false;
|
|
276
|
+
|
|
277
|
+
const dt = directText(el);
|
|
278
|
+
const a = norm(el.getAttribute?.("aria-label"));
|
|
279
|
+
const t = norm(el.getAttribute?.("title"));
|
|
280
|
+
const alt = norm(el.getAttribute?.("alt"));
|
|
281
|
+
const ph = norm(el.getAttribute?.("placeholder"));
|
|
282
|
+
const hasStrong = !!(dt || a || t || alt || ph);
|
|
283
|
+
if (!hasStrong) return true;
|
|
284
|
+
|
|
285
|
+
// still scan when label is basically only a class stub / generic tokens
|
|
286
|
+
const s = norm(baseName);
|
|
287
|
+
if (!s) return true;
|
|
288
|
+
if (/^class=/.test(s) && s.length < 40) return true;
|
|
289
|
+
return false;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const labelTextOf = (el) => {
|
|
293
|
+
const base = baseLabelTextOf(el);
|
|
294
|
+
if (needsIconScan(el, base)) {
|
|
295
|
+
const desc = iconDescriptorFromDescendants(el);
|
|
296
|
+
return desc ? (base ? `${base} | ${desc}` : desc) : base;
|
|
297
|
+
}
|
|
298
|
+
return base;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const mergeIntoName = (entry, extra) => {
|
|
302
|
+
const v = norm(extra);
|
|
303
|
+
if (!v) return;
|
|
304
|
+
if (!entry.name) entry.name = v;
|
|
305
|
+
else if (!entry.name.includes(v)) entry.name = `${entry.name} | ${v}`;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const fastIntersectInfo = (el) => {
|
|
309
|
+
if (!el || el.nodeType !== 1) return { ok: false };
|
|
310
|
+
if (isSkippableTag(el)) return { ok: false };
|
|
311
|
+
const r = getCachedRect(el);
|
|
312
|
+
if (!r || r.width < 1 || r.height < 1) return { ok: false };
|
|
313
|
+
const ib = intersectViewport(r);
|
|
314
|
+
if (ib.width < 1 || ib.height < 1) return { ok: false };
|
|
315
|
+
return { ok: true, rect: r, ibox: ib };
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const isRenderable = (el) => {
|
|
319
|
+
const bits = getStyleBits(el);
|
|
320
|
+
if (bits.display === "none" || bits.visibility === "hidden") return false;
|
|
321
|
+
if (parseFloat(bits.opacity || "1") === 0) return false;
|
|
322
|
+
return true;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const samplePointsAdaptive = (ibox, el) => {
|
|
326
|
+
if (ibox.width < 1 || ibox.height < 1) return [];
|
|
327
|
+
const a = ibox.width * ibox.height;
|
|
328
|
+
const inset = 2;
|
|
329
|
+
const cx = ibox.x + ibox.width / 2;
|
|
330
|
+
const cy = ibox.y + ibox.height / 2;
|
|
331
|
+
|
|
332
|
+
// dynamic sampling:
|
|
333
|
+
// - very large regions: center only
|
|
334
|
+
// - medium: center + 2 corners
|
|
335
|
+
// - small: 5 points
|
|
336
|
+
let mode = 5;
|
|
337
|
+
if (a >= 150 * 150) mode = 1;
|
|
338
|
+
else if (a >= 60 * 60) mode = 3;
|
|
339
|
+
|
|
340
|
+
// controls tend to be small/precise; keep stronger checks
|
|
341
|
+
if (isControlLike(el) && mode < 5) mode = 5;
|
|
342
|
+
|
|
343
|
+
const x1 = ibox.x + inset;
|
|
344
|
+
const y1 = ibox.y + inset;
|
|
345
|
+
const x2 = ibox.x + ibox.width - inset;
|
|
346
|
+
const y2 = ibox.y + ibox.height - inset;
|
|
347
|
+
|
|
348
|
+
const pts = [];
|
|
349
|
+
pts.push([cx, cy]);
|
|
350
|
+
if (mode >= 3) {
|
|
351
|
+
pts.push([x1, y1]);
|
|
352
|
+
pts.push([x2, y2]);
|
|
353
|
+
}
|
|
354
|
+
if (mode >= 5) {
|
|
355
|
+
pts.push([x2, y1]);
|
|
356
|
+
pts.push([x1, y2]);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const out = [];
|
|
360
|
+
for (const [x, y] of pts) {
|
|
361
|
+
out.push([clamp(x, 0, Math.max(0, vw - 1)), clamp(y, 0, Math.max(0, vh - 1))]);
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const notFullyOccluded = (el, ibox) => {
|
|
367
|
+
const pts = samplePointsAdaptive(ibox, el);
|
|
368
|
+
if (!pts.length) return false;
|
|
369
|
+
for (const [x, y] of pts) {
|
|
370
|
+
const top = document.elementFromPoint(x, y);
|
|
371
|
+
if (!top) continue;
|
|
372
|
+
if (top === el || el.contains(top)) return true;
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const visibleInfo = (el) => {
|
|
378
|
+
const fi = fastIntersectInfo(el);
|
|
379
|
+
if (!fi.ok) return { ok: false };
|
|
380
|
+
|
|
381
|
+
if (!isRenderable(el)) return { ok: false };
|
|
382
|
+
|
|
383
|
+
if (!notFullyOccluded(el, fi.ibox)) return { ok: false };
|
|
384
|
+
|
|
385
|
+
return { ok: true, rect: fi.rect, ibox: fi.ibox };
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const childrenForTraversal = (el) => {
|
|
389
|
+
const out = [];
|
|
390
|
+
const pushKids = (parent) => {
|
|
391
|
+
try {
|
|
392
|
+
const kids = parent.children ? Array.from(parent.children) : [];
|
|
393
|
+
for (const c of kids) {
|
|
394
|
+
if (isSkippableTag(c)) continue;
|
|
395
|
+
|
|
396
|
+
const fi = fastIntersectInfo(c);
|
|
397
|
+
if (fi.ok) { out.push(c); continue; }
|
|
398
|
+
|
|
399
|
+
// "tunnel" cases: 0×0 wrappers that may contain visible grandchildren
|
|
400
|
+
try {
|
|
401
|
+
const bits = getStyleBits(c);
|
|
402
|
+
const hasKids = (c.children && c.children.length) || (c.shadowRoot && c.shadowRoot.children && c.shadowRoot.children.length);
|
|
403
|
+
if ((bits.display === "contents") || hasKids) out.push(c);
|
|
404
|
+
} catch {
|
|
405
|
+
// if we can't read style, but it has children, still worth tunneling
|
|
406
|
+
if (c.children && c.children.length) out.push(c);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch {}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
pushKids(el);
|
|
413
|
+
|
|
414
|
+
// include shadow root children too
|
|
415
|
+
try {
|
|
416
|
+
const sr = el.shadowRoot;
|
|
417
|
+
if (sr) pushKids(sr);
|
|
418
|
+
} catch {}
|
|
419
|
+
|
|
420
|
+
return out;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const visibleChildrenStrict = (el) => {
|
|
424
|
+
const out = [];
|
|
425
|
+
try {
|
|
426
|
+
const kids = el.children ? Array.from(el.children) : [];
|
|
427
|
+
for (const c of kids) {
|
|
428
|
+
const v = visibleInfo(c);
|
|
429
|
+
if (v.ok) out.push(c);
|
|
430
|
+
}
|
|
431
|
+
} catch {}
|
|
432
|
+
try {
|
|
433
|
+
const sr = el.shadowRoot;
|
|
434
|
+
if (sr && sr.children) {
|
|
435
|
+
for (const c of Array.from(sr.children)) {
|
|
436
|
+
const v = visibleInfo(c);
|
|
437
|
+
if (v.ok) out.push(c);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} catch {}
|
|
441
|
+
return out;
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const area = (b) => Math.max(0, b.width) * Math.max(0, b.height);
|
|
445
|
+
const intersectArea = (a, b) => {
|
|
446
|
+
const x1 = Math.max(a.x, b.x);
|
|
447
|
+
const y1 = Math.max(a.y, b.y);
|
|
448
|
+
const x2 = Math.min(a.x + a.width, b.x + b.width);
|
|
449
|
+
const y2 = Math.min(a.y + a.height, b.y + b.height);
|
|
450
|
+
const w = Math.max(0, x2 - x1);
|
|
451
|
+
const h = Math.max(0, y2 - y1);
|
|
452
|
+
return w * h;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const isUselessWrapper = (el, child) => {
|
|
456
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
457
|
+
if (!WRAPPER_TAGS.has(tag)) return false;
|
|
458
|
+
if (isImportant(el)) return false;
|
|
459
|
+
|
|
460
|
+
if (el.getAttribute?.("id")) return false;
|
|
461
|
+
if (roleOf(el)) return false;
|
|
462
|
+
const tabindex = el.getAttribute?.("tabindex");
|
|
463
|
+
if (tabindex && tabindex !== "-1") return false;
|
|
464
|
+
if (el.hasAttribute?.("onclick")) return false;
|
|
465
|
+
|
|
466
|
+
const f1 = fastIntersectInfo(el);
|
|
467
|
+
const f2 = fastIntersectInfo(child);
|
|
468
|
+
if (!f1.ok || !f2.ok) return false;
|
|
469
|
+
|
|
470
|
+
const b1 = f1.ibox, b2 = f2.ibox;
|
|
471
|
+
const a1 = area(b1), a2 = area(b2);
|
|
472
|
+
if (a1 < 1 || a2 < 1) return false;
|
|
473
|
+
const ia = intersectArea(b1, b2);
|
|
474
|
+
const overlap = ia / Math.min(a1, a2);
|
|
475
|
+
return overlap >= 0.92;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const resolveRepresentative = (el) => {
|
|
479
|
+
let cur = el;
|
|
480
|
+
for (let steps = 0; steps < 50; steps++) {
|
|
481
|
+
const kids = childrenForTraversal(cur);
|
|
482
|
+
if (kids.length !== 1) break;
|
|
483
|
+
const child = kids[0];
|
|
484
|
+
if (isUselessWrapper(cur, child)) { cur = child; continue; }
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
return cur;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const nearestVisibleControlAncestor = (el) => {
|
|
491
|
+
let cur = el?.parentElement || null;
|
|
492
|
+
while (cur && cur !== document.body) {
|
|
493
|
+
if (isControlLike(cur)) {
|
|
494
|
+
const v = visibleInfo(cur);
|
|
495
|
+
if (v.ok) return cur;
|
|
496
|
+
}
|
|
497
|
+
cur = cur.parentElement;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const shouldEmit = (el) => {
|
|
503
|
+
if (isControlLike(el)) return true;
|
|
504
|
+
|
|
505
|
+
// text-heavy, misused divs/spans:
|
|
506
|
+
// emit if it has meaningful direct text and isn't within a visible control
|
|
507
|
+
const dt = directText(el);
|
|
508
|
+
if (dt && dt.length >= 2) {
|
|
509
|
+
const ctrl = nearestVisibleControlAncestor(el);
|
|
510
|
+
if (!ctrl) return true;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (isImportant(el)) {
|
|
514
|
+
const ctrl = nearestVisibleControlAncestor(el);
|
|
515
|
+
return !ctrl;
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const raw = [];
|
|
521
|
+
const byId = new Map();
|
|
522
|
+
const idOf = new WeakMap();
|
|
523
|
+
let nextId = 0;
|
|
524
|
+
|
|
525
|
+
const addNode = (el, parentId, depth, ibox) => {
|
|
526
|
+
const id = nextId++;
|
|
527
|
+
idOf.set(el, id);
|
|
528
|
+
const entry = {
|
|
529
|
+
id,
|
|
530
|
+
el,
|
|
531
|
+
parent_id: parentId,
|
|
532
|
+
depth,
|
|
533
|
+
tag: (el.tagName || "").toLowerCase(),
|
|
534
|
+
role: roleOf(el),
|
|
535
|
+
name: labelTextOf(el),
|
|
536
|
+
bbox_viewport: {
|
|
537
|
+
x: Math.round(ibox.x),
|
|
538
|
+
y: Math.round(ibox.y),
|
|
539
|
+
width: Math.round(ibox.width),
|
|
540
|
+
height: Math.round(ibox.height),
|
|
541
|
+
},
|
|
542
|
+
bbox_page: {
|
|
543
|
+
x: Math.round(ibox.x + sx),
|
|
544
|
+
y: Math.round(ibox.y + sy),
|
|
545
|
+
width: Math.round(ibox.width),
|
|
546
|
+
height: Math.round(ibox.height),
|
|
547
|
+
},
|
|
548
|
+
attributes: attrsOf(el),
|
|
549
|
+
dom_path: domPath(el),
|
|
550
|
+
};
|
|
551
|
+
raw.push(entry);
|
|
552
|
+
byId.set(id, entry);
|
|
553
|
+
return entry;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const walk = (startEl, parentEntry, depth) => {
|
|
557
|
+
if (!startEl || startEl.nodeType !== 1) return;
|
|
558
|
+
|
|
559
|
+
// prune early by viewport intersection
|
|
560
|
+
const fi = fastIntersectInfo(startEl);
|
|
561
|
+
if (!fi.ok) {
|
|
562
|
+
// IMPORTANT: wrapper may be 0x0 (e.g. display:contents) but have visible descendants.
|
|
563
|
+
const kids = childrenForTraversal(startEl);
|
|
564
|
+
for (const c of kids) walk(c, parentEntry, depth);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const rep = resolveRepresentative(startEl);
|
|
569
|
+
if (!rep || rep.nodeType !== 1) return;
|
|
570
|
+
|
|
571
|
+
const v = visibleInfo(rep);
|
|
572
|
+
if (!v.ok) {
|
|
573
|
+
// rep itself may fail occlusion / rect tests even though descendants are visible
|
|
574
|
+
const kids = childrenForTraversal(rep);
|
|
575
|
+
for (const c of kids) walk(c, parentEntry, depth);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const ctrl = (!isControlLike(rep)) ? nearestVisibleControlAncestor(rep) : null;
|
|
580
|
+
if (ctrl) {
|
|
581
|
+
const cv = visibleInfo(ctrl);
|
|
582
|
+
if (cv.ok) {
|
|
583
|
+
let ctrlEntry = null;
|
|
584
|
+
const existingId = idOf.get(ctrl);
|
|
585
|
+
if (existingId !== undefined) ctrlEntry = byId.get(existingId) || null;
|
|
586
|
+
if (!ctrlEntry) ctrlEntry = addNode(ctrl, parentEntry ? parentEntry.id : null, depth, cv.ibox);
|
|
587
|
+
|
|
588
|
+
// merge rep's base label always; scan icons only when needed
|
|
589
|
+
const repBase = baseLabelTextOf(rep);
|
|
590
|
+
if (repBase) mergeIntoName(ctrlEntry, repBase);
|
|
591
|
+
|
|
592
|
+
if (needsIconScan(ctrl, baseLabelTextOf(ctrl)) || needsIconScan(rep, repBase)) {
|
|
593
|
+
const repIcon = iconDescriptorFromDescendants(rep);
|
|
594
|
+
if (repIcon) mergeIntoName(ctrlEntry, repIcon);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (!ctrlEntry.attributes) ctrlEntry.attributes = {};
|
|
598
|
+
if (needsIconScan(ctrl, baseLabelTextOf(ctrl))) {
|
|
599
|
+
const iconDesc = norm(iconDescriptorFromDescendants(ctrl));
|
|
600
|
+
if (iconDesc) ctrlEntry.attributes["icon-desc"] = iconDesc;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const kids = childrenForTraversal(rep);
|
|
604
|
+
for (const c of kids) walk(c, ctrlEntry, ctrlEntry.depth + 1);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
let myEntry = parentEntry;
|
|
610
|
+
let nextDepth = depth;
|
|
611
|
+
|
|
612
|
+
if (shouldEmit(rep)) {
|
|
613
|
+
myEntry = addNode(rep, parentEntry ? parentEntry.id : null, depth, v.ibox);
|
|
614
|
+
nextDepth = depth + 1;
|
|
615
|
+
|
|
616
|
+
if (isControlLike(rep) && needsIconScan(rep, baseLabelTextOf(rep))) {
|
|
617
|
+
const iconDesc = norm(iconDescriptorFromDescendants(rep));
|
|
618
|
+
if (iconDesc) {
|
|
619
|
+
mergeIntoName(myEntry, iconDesc);
|
|
620
|
+
myEntry.attributes["icon-desc"] = iconDesc;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const kids = childrenForTraversal(rep);
|
|
626
|
+
for (const c of kids) walk(c, myEntry, nextDepth);
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const root = document.body || document.documentElement;
|
|
630
|
+
if (!root) return [];
|
|
631
|
+
|
|
632
|
+
const topKids = childrenForTraversal(root);
|
|
633
|
+
for (const c of topKids) walk(c, null, 0);
|
|
634
|
+
|
|
635
|
+
if (!raw.length) return [];
|
|
636
|
+
|
|
637
|
+
// merge labels into controls; remove labels
|
|
638
|
+
const keptIds = new Set(raw.map(e => e.id));
|
|
639
|
+
const domToVisibleId = new WeakMap();
|
|
640
|
+
for (const e of raw) domToVisibleId.set(e.el, e.id);
|
|
641
|
+
|
|
642
|
+
const isLabelTag = (el) => (el?.tagName || "").toLowerCase() === "label";
|
|
643
|
+
|
|
644
|
+
const mergeLabelIntoControl = (controlId, labelId, text) => {
|
|
645
|
+
const c = byId.get(controlId);
|
|
646
|
+
const l = byId.get(labelId);
|
|
647
|
+
if (!c || !l) return;
|
|
648
|
+
if (!keptIds.has(controlId) || !keptIds.has(labelId)) return;
|
|
649
|
+
const t = norm(text || l.name || "");
|
|
650
|
+
if (!t) return;
|
|
651
|
+
mergeIntoName(c, t);
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const labelRemoval = new Set();
|
|
655
|
+
const controls = raw.filter(e => keptIds.has(e.id) && e.el && isControlLike(e.el));
|
|
656
|
+
|
|
657
|
+
for (const c of controls) {
|
|
658
|
+
try {
|
|
659
|
+
const labs = c.el.labels ? Array.from(c.el.labels) : [];
|
|
660
|
+
for (const labEl of labs) {
|
|
661
|
+
const lid = domToVisibleId.get(labEl);
|
|
662
|
+
if (lid !== undefined && keptIds.has(lid)) {
|
|
663
|
+
mergeLabelIntoControl(c.id, lid, norm(labEl.innerText || labEl.textContent || ""));
|
|
664
|
+
labelRemoval.add(lid);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} catch {}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
for (const c of controls) {
|
|
671
|
+
const v = c.el.getAttribute?.("aria-labelledby");
|
|
672
|
+
if (!v) continue;
|
|
673
|
+
const ids = v.split(/\s+/).map(s => s.trim()).filter(Boolean);
|
|
674
|
+
for (const domId of ids) {
|
|
675
|
+
const labEl = document.getElementById(domId);
|
|
676
|
+
if (!labEl) continue;
|
|
677
|
+
const lid = domToVisibleId.get(labEl);
|
|
678
|
+
if (lid !== undefined && keptIds.has(lid)) {
|
|
679
|
+
mergeLabelIntoControl(c.id, lid, norm(labEl.innerText || labEl.textContent || ""));
|
|
680
|
+
labelRemoval.add(lid);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
for (const e of raw) {
|
|
686
|
+
if (!keptIds.has(e.id)) continue;
|
|
687
|
+
if (!e.el || !isLabelTag(e.el)) continue;
|
|
688
|
+
const f = e.el.getAttribute?.("for");
|
|
689
|
+
if (!f) continue;
|
|
690
|
+
const target = document.getElementById(f);
|
|
691
|
+
if (!target) continue;
|
|
692
|
+
const tid = domToVisibleId.get(target);
|
|
693
|
+
if (tid !== undefined && keptIds.has(tid)) {
|
|
694
|
+
mergeLabelIntoControl(tid, e.id, norm(e.el.innerText || e.el.textContent || ""));
|
|
695
|
+
labelRemoval.add(e.id);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
for (const lid of labelRemoval) keptIds.delete(lid);
|
|
700
|
+
|
|
701
|
+
// recompute parent/depth
|
|
702
|
+
const findNearestKeptAncestorId = (el) => {
|
|
703
|
+
let cur = el;
|
|
704
|
+
while (cur) {
|
|
705
|
+
let p = cur.parentElement;
|
|
706
|
+
while (p) {
|
|
707
|
+
const pid = idOf.get(p);
|
|
708
|
+
if (pid !== undefined && keptIds.has(pid)) return pid;
|
|
709
|
+
p = p.parentElement;
|
|
710
|
+
}
|
|
711
|
+
const rn = cur.getRootNode?.();
|
|
712
|
+
if (rn && rn.host) { cur = rn.host; continue; }
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
return null;
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
for (const e of raw) {
|
|
719
|
+
if (!keptIds.has(e.id)) continue;
|
|
720
|
+
e.parent_id = findNearestKeptAncestorId(e.el);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const depthMemo = new Map();
|
|
724
|
+
const depthOf = (id) => {
|
|
725
|
+
if (depthMemo.has(id)) return depthMemo.get(id);
|
|
726
|
+
const e = byId.get(id);
|
|
727
|
+
if (!e || !keptIds.has(id)) return 0;
|
|
728
|
+
const p = e.parent_id;
|
|
729
|
+
const d = p === null ? 0 : depthOf(p) + 1;
|
|
730
|
+
depthMemo.set(id, d);
|
|
731
|
+
return d;
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
for (const e of raw) {
|
|
735
|
+
if (!keptIds.has(e.id)) continue;
|
|
736
|
+
e.depth = depthOf(e.id);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const kept = raw.filter(e => keptIds.has(e.id));
|
|
740
|
+
kept.sort((a,b) => {
|
|
741
|
+
const ay = a.bbox_viewport?.y ?? 0;
|
|
742
|
+
const by = b.bbox_viewport?.y ?? 0;
|
|
743
|
+
if (ay !== by) return ay - by;
|
|
744
|
+
const ax = a.bbox_viewport?.x ?? 0;
|
|
745
|
+
const bx = b.bbox_viewport?.x ?? 0;
|
|
746
|
+
if (ax !== bx) return ax - bx;
|
|
747
|
+
const aa = (a.bbox_viewport?.width ?? 0) * (a.bbox_viewport?.height ?? 0);
|
|
748
|
+
const ba = (b.bbox_viewport?.width ?? 0) * (b.bbox_viewport?.height ?? 0);
|
|
749
|
+
return aa - ba;
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
return kept.map(e => ({
|
|
753
|
+
id: e.id,
|
|
754
|
+
parent_id: e.parent_id,
|
|
755
|
+
depth: e.depth,
|
|
756
|
+
tag: e.tag,
|
|
757
|
+
role: e.role,
|
|
758
|
+
name: e.name || "",
|
|
759
|
+
bbox_viewport: e.bbox_viewport,
|
|
760
|
+
bbox_page: e.bbox_page,
|
|
761
|
+
attributes: e.attributes,
|
|
762
|
+
dom_path: e.dom_path,
|
|
763
|
+
}));
|
|
764
|
+
}
|