poke-browser 0.2.8
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 +60 -0
- package/cli.mjs +457 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +23 -0
- package/dist/logger.js.map +1 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +279 -0
- package/dist/run.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +13 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +707 -0
- package/dist/tools.js.map +1 -0
- package/dist/transport.d.ts +60 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +387 -0
- package/dist/transport.js.map +1 -0
- package/extension/background.js +1998 -0
- package/extension/content.js +1416 -0
- package/extension/icon.png +0 -0
- package/extension/manifest.json +48 -0
- package/extension/offscreen.html +10 -0
- package/extension/offscreen.js +246 -0
- package/extension/popup.html +426 -0
- package/extension/popup.js +205 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relays automation commands from the service worker into the page.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const CONSOLE_RING_MAX = 500;
|
|
6
|
+
const PAGE_ERROR_RING_MAX = 200;
|
|
7
|
+
|
|
8
|
+
/** @type {Array<{ level: string; message: string; timestamp: number; stack?: string }>} */
|
|
9
|
+
let consoleRing = [];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Uncaught errors and unhandled rejections (separate from console ring).
|
|
13
|
+
* @type {Array<{ kind: string; message: string; stack?: string; filename?: string; lineno?: number; colno?: number; timestamp: number }>}
|
|
14
|
+
*/
|
|
15
|
+
let pageErrorRing = [];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {{ kind: string; message: string; stack?: string; filename?: string; lineno?: number; colno?: number; timestamp: number }} entry
|
|
19
|
+
*/
|
|
20
|
+
function pushPageError(entry) {
|
|
21
|
+
pageErrorRing.push(entry);
|
|
22
|
+
while (pageErrorRing.length > PAGE_ERROR_RING_MAX) pageErrorRing.shift();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
window.addEventListener("error", (ev) => {
|
|
26
|
+
try {
|
|
27
|
+
pushPageError({
|
|
28
|
+
kind: "error",
|
|
29
|
+
message: ev.message || String(ev.error || "error"),
|
|
30
|
+
stack: ev.error instanceof Error ? ev.error.stack : undefined,
|
|
31
|
+
filename: ev.filename,
|
|
32
|
+
lineno: ev.lineno,
|
|
33
|
+
colno: ev.colno,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
/* ignore */
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
window.addEventListener("unhandledrejection", (ev) => {
|
|
42
|
+
try {
|
|
43
|
+
const reason = ev.reason;
|
|
44
|
+
const message =
|
|
45
|
+
reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
|
|
46
|
+
const stack = reason instanceof Error ? reason.stack : undefined;
|
|
47
|
+
pushPageError({
|
|
48
|
+
kind: "unhandledrejection",
|
|
49
|
+
message,
|
|
50
|
+
stack,
|
|
51
|
+
timestamp: Date.now(),
|
|
52
|
+
});
|
|
53
|
+
} catch {
|
|
54
|
+
/* ignore */
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {unknown} a
|
|
60
|
+
*/
|
|
61
|
+
function formatConsoleArg(a) {
|
|
62
|
+
if (a instanceof Error) return a.stack || a.message;
|
|
63
|
+
if (typeof a === "object" && a !== null) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.stringify(a);
|
|
66
|
+
} catch {
|
|
67
|
+
return String(a);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return String(a);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} level
|
|
75
|
+
* @param {unknown[]} args
|
|
76
|
+
*/
|
|
77
|
+
function pushConsoleEntry(level, args) {
|
|
78
|
+
const message = args.map(formatConsoleArg).join(" ").slice(0, 20000);
|
|
79
|
+
const errArg = args.find((x) => x instanceof Error);
|
|
80
|
+
consoleRing.push({
|
|
81
|
+
level,
|
|
82
|
+
message,
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
stack: errArg instanceof Error ? errArg.stack : undefined,
|
|
85
|
+
});
|
|
86
|
+
while (consoleRing.length > CONSOLE_RING_MAX) consoleRing.shift();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
["log", "info", "warn", "error"].forEach((level) => {
|
|
90
|
+
const orig = console[level].bind(console);
|
|
91
|
+
console[level] = function pokeConsolePatched(...args) {
|
|
92
|
+
try {
|
|
93
|
+
pushConsoleEntry(level, args);
|
|
94
|
+
} catch {
|
|
95
|
+
/* ignore ring failures */
|
|
96
|
+
}
|
|
97
|
+
orig(...args);
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Query selector across the document tree and inside open shadow roots (same-document; does not cross iframes).
|
|
103
|
+
* @param {Document | ShadowRoot | Element} root
|
|
104
|
+
* @param {string} selector
|
|
105
|
+
* @returns {Element[]}
|
|
106
|
+
*/
|
|
107
|
+
function deepQueryAll(root, selector) {
|
|
108
|
+
/** @type {Element[]} */
|
|
109
|
+
const results = [];
|
|
110
|
+
try {
|
|
111
|
+
results.push(...root.querySelectorAll(selector));
|
|
112
|
+
} catch {
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
for (const el of root.querySelectorAll("*")) {
|
|
116
|
+
if (el.shadowRoot) {
|
|
117
|
+
results.push(...deepQueryAll(el.shadowRoot, selector));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* XPath across the light tree and each open shadow root (shadow evaluated with that root as context).
|
|
125
|
+
* @param {string} expr
|
|
126
|
+
* @returns {Element[]}
|
|
127
|
+
*/
|
|
128
|
+
function deepXPathAll(expr) {
|
|
129
|
+
/** @type {Element[]} */
|
|
130
|
+
const out = [];
|
|
131
|
+
/**
|
|
132
|
+
* @param {Document | ShadowRoot} context
|
|
133
|
+
*/
|
|
134
|
+
function collectFrom(context) {
|
|
135
|
+
try {
|
|
136
|
+
const r = document.evaluate(expr, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
137
|
+
for (let i = 0; i < r.snapshotLength; i++) {
|
|
138
|
+
const n = r.snapshotItem(i);
|
|
139
|
+
if (n instanceof Element) out.push(n);
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
/* ignore */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
collectFrom(document);
|
|
146
|
+
/**
|
|
147
|
+
* @param {Element} el
|
|
148
|
+
*/
|
|
149
|
+
function walk(el) {
|
|
150
|
+
if (el.shadowRoot) {
|
|
151
|
+
collectFrom(el.shadowRoot);
|
|
152
|
+
for (const c of el.shadowRoot.children) walk(c);
|
|
153
|
+
}
|
|
154
|
+
for (const c of el.children) walk(c);
|
|
155
|
+
}
|
|
156
|
+
if (document.documentElement) walk(document.documentElement);
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} selector
|
|
162
|
+
* @returns {Element | null}
|
|
163
|
+
*/
|
|
164
|
+
function querySelectorOrXPath(selector) {
|
|
165
|
+
const s = selector.trim();
|
|
166
|
+
if (s.startsWith("//") || s.toLowerCase().startsWith("xpath:")) {
|
|
167
|
+
const expr = s.toLowerCase().startsWith("xpath:") ? s.slice(6).trim() : s;
|
|
168
|
+
const all = deepXPathAll(expr);
|
|
169
|
+
return all[0] ?? null;
|
|
170
|
+
}
|
|
171
|
+
const all = deepQueryAll(document, s);
|
|
172
|
+
return all[0] ?? null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {Element} el
|
|
177
|
+
*/
|
|
178
|
+
function elementSummary(el) {
|
|
179
|
+
const tag = el.tagName.toLowerCase();
|
|
180
|
+
const id = el.id || undefined;
|
|
181
|
+
const classes = typeof el.className === "string" ? el.className : "";
|
|
182
|
+
let text = "";
|
|
183
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
184
|
+
text = el.value?.slice(0, 200) ?? "";
|
|
185
|
+
} else {
|
|
186
|
+
text = (el.textContent || "").trim().slice(0, 200);
|
|
187
|
+
}
|
|
188
|
+
return { tag, id, classes, text };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Viewport client coordinates used as the synthetic click anchor (same as syntheticClick).
|
|
193
|
+
* @param {Element} el
|
|
194
|
+
*/
|
|
195
|
+
function getSyntheticClickClientPoint(el) {
|
|
196
|
+
const r = el.getBoundingClientRect();
|
|
197
|
+
return {
|
|
198
|
+
x: r.left + Math.min(r.width / 2, 50),
|
|
199
|
+
y: r.top + Math.min(r.height / 2, 50),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {Element} el
|
|
205
|
+
*/
|
|
206
|
+
function syntheticClick(el) {
|
|
207
|
+
const { x, y } = getSyntheticClickClientPoint(el);
|
|
208
|
+
const init = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
|
|
209
|
+
el.dispatchEvent(new MouseEvent("mousedown", init));
|
|
210
|
+
el.dispatchEvent(new MouseEvent("mouseup", init));
|
|
211
|
+
if (typeof el.click === "function") el.click();
|
|
212
|
+
else el.dispatchEvent(new MouseEvent("click", init));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {unknown} message
|
|
217
|
+
* @param {(r: unknown) => void} sendResponse
|
|
218
|
+
*/
|
|
219
|
+
function handleClickElement(message, sendResponse) {
|
|
220
|
+
const m = /** @type {{ selector?: string }} */ (message);
|
|
221
|
+
const selector = typeof m.selector === "string" ? m.selector : "";
|
|
222
|
+
if (!selector) {
|
|
223
|
+
sendResponse({ success: false, error: "Missing selector" });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const el = querySelectorOrXPath(selector);
|
|
227
|
+
if (!el) {
|
|
228
|
+
sendResponse({ success: false, error: "Element not found" });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
syntheticClick(el);
|
|
233
|
+
sendResponse({ success: true, element: elementSummary(el) });
|
|
234
|
+
} catch (err) {
|
|
235
|
+
sendResponse({ success: false, error: String(err) });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {unknown} message
|
|
241
|
+
* @param {(r: unknown) => void} sendResponse
|
|
242
|
+
*/
|
|
243
|
+
function handleResolveClickPoint(message, sendResponse) {
|
|
244
|
+
const m = /** @type {{ selector?: string }} */ (message);
|
|
245
|
+
const selector = typeof m.selector === "string" ? m.selector : "";
|
|
246
|
+
if (!selector) {
|
|
247
|
+
sendResponse({ success: false, error: "Missing selector" });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const el = querySelectorOrXPath(selector);
|
|
251
|
+
if (!el) {
|
|
252
|
+
sendResponse({ success: false, error: "Element not found" });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const { x, y } = getSyntheticClickClientPoint(el);
|
|
256
|
+
sendResponse({ success: true, x, y });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {unknown} message
|
|
261
|
+
* @param {(r: unknown) => void} sendResponse
|
|
262
|
+
*/
|
|
263
|
+
function handleTypeText(message, sendResponse) {
|
|
264
|
+
const m = /** @type {{ text?: string; selector?: string; clear?: boolean }} */ (message);
|
|
265
|
+
const text = typeof m.text === "string" ? m.text : "";
|
|
266
|
+
const shouldClear = m.clear !== false;
|
|
267
|
+
let el = null;
|
|
268
|
+
if (typeof m.selector === "string" && m.selector.trim()) {
|
|
269
|
+
el = querySelectorOrXPath(m.selector);
|
|
270
|
+
} else {
|
|
271
|
+
const a = document.activeElement;
|
|
272
|
+
el = a instanceof Element ? a : null;
|
|
273
|
+
}
|
|
274
|
+
if (!el || !(el instanceof HTMLElement)) {
|
|
275
|
+
sendResponse({ success: false, charsTyped: 0 });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
if (el.isContentEditable) {
|
|
281
|
+
el.focus();
|
|
282
|
+
if (shouldClear) {
|
|
283
|
+
const sel = window.getSelection();
|
|
284
|
+
if (sel && el.firstChild) {
|
|
285
|
+
const range = document.createRange();
|
|
286
|
+
range.selectNodeContents(el);
|
|
287
|
+
sel.removeAllRanges();
|
|
288
|
+
sel.addRange(range);
|
|
289
|
+
}
|
|
290
|
+
document.execCommand("delete");
|
|
291
|
+
el.textContent = text;
|
|
292
|
+
} else {
|
|
293
|
+
el.textContent = (el.textContent || "") + text;
|
|
294
|
+
}
|
|
295
|
+
el.dispatchEvent(new InputEvent("input", { bubbles: true, data: text, inputType: "insertText" }));
|
|
296
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
297
|
+
sendResponse({ success: true, charsTyped: text.length });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const tag = el.tagName.toLowerCase();
|
|
302
|
+
if (tag === "input" || tag === "textarea") {
|
|
303
|
+
const input = /** @type {HTMLInputElement | HTMLTextAreaElement} */ (el);
|
|
304
|
+
input.focus();
|
|
305
|
+
if (shouldClear) {
|
|
306
|
+
input.select();
|
|
307
|
+
document.execCommand("delete");
|
|
308
|
+
input.value = text;
|
|
309
|
+
} else {
|
|
310
|
+
input.value = (input.value || "") + text;
|
|
311
|
+
}
|
|
312
|
+
input.dispatchEvent(new InputEvent("input", { bubbles: true, data: text, inputType: "insertText" }));
|
|
313
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
314
|
+
sendResponse({ success: true, charsTyped: text.length });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
sendResponse({ success: false, charsTyped: 0 });
|
|
319
|
+
} catch (err) {
|
|
320
|
+
sendResponse({ success: false, charsTyped: 0, error: String(err) });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {unknown} message
|
|
326
|
+
* @param {(r: unknown) => void} sendResponse
|
|
327
|
+
*/
|
|
328
|
+
function handleScrollWindow(message, sendResponse) {
|
|
329
|
+
const m = /** @type {{ payload?: Record<string, unknown> }} */ (message);
|
|
330
|
+
const p = m.payload && typeof m.payload === "object" ? m.payload : {};
|
|
331
|
+
const behavior = p.behavior === "smooth" ? "smooth" : "auto";
|
|
332
|
+
const selector = typeof p.selector === "string" ? p.selector.trim() : "";
|
|
333
|
+
const dirRaw = typeof p.direction === "string" ? p.direction.toLowerCase() : "";
|
|
334
|
+
const dir =
|
|
335
|
+
dirRaw === "up" || dirRaw === "down" || dirRaw === "left" || dirRaw === "right" ? dirRaw : "";
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
if (selector) {
|
|
339
|
+
const el = querySelectorOrXPath(selector);
|
|
340
|
+
if (!el) {
|
|
341
|
+
sendResponse({ success: false, scrollX: window.scrollX, scrollY: window.scrollY, error: "Element not found" });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
el.scrollIntoView({ behavior, block: "center", inline: "nearest" });
|
|
345
|
+
} else if (typeof p.x === "number" || typeof p.y === "number") {
|
|
346
|
+
const left = typeof p.x === "number" ? p.x : window.scrollX;
|
|
347
|
+
const top = typeof p.y === "number" ? p.y : window.scrollY;
|
|
348
|
+
window.scrollTo({ left, top, behavior });
|
|
349
|
+
} else {
|
|
350
|
+
let dx = typeof p.deltaX === "number" && Number.isFinite(p.deltaX) ? p.deltaX : 0;
|
|
351
|
+
let dy = typeof p.deltaY === "number" && Number.isFinite(p.deltaY) ? p.deltaY : 0;
|
|
352
|
+
if (dir) {
|
|
353
|
+
let amt = typeof p.amount === "number" && Number.isFinite(p.amount) ? Math.abs(p.amount) : NaN;
|
|
354
|
+
if (!Number.isFinite(amt) || amt === 0) {
|
|
355
|
+
if (dir === "up" || dir === "down") {
|
|
356
|
+
const fromDelta = typeof p.deltaY === "number" && Number.isFinite(p.deltaY) && p.deltaY !== 0;
|
|
357
|
+
amt = fromDelta ? Math.abs(p.deltaY) : Math.max(200, Math.floor(window.innerHeight * 0.85));
|
|
358
|
+
} else {
|
|
359
|
+
const fromDelta = typeof p.deltaX === "number" && Number.isFinite(p.deltaX) && p.deltaX !== 0;
|
|
360
|
+
amt = fromDelta ? Math.abs(p.deltaX) : Math.max(200, Math.floor(window.innerWidth * 0.85));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
dx = dir === "left" ? -amt : dir === "right" ? amt : 0;
|
|
364
|
+
dy = dir === "up" ? -amt : dir === "down" ? amt : 0;
|
|
365
|
+
}
|
|
366
|
+
window.scrollBy({ left: dx, top: dy, behavior });
|
|
367
|
+
}
|
|
368
|
+
sendResponse({ success: true, scrollX: window.scrollX, scrollY: window.scrollY });
|
|
369
|
+
} catch (err) {
|
|
370
|
+
sendResponse({ success: false, scrollX: window.scrollX, scrollY: window.scrollY, error: String(err) });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @param {unknown} message
|
|
376
|
+
* @param {(r: unknown) => void} sendResponse
|
|
377
|
+
*/
|
|
378
|
+
function handleEval(message, sendResponse) {
|
|
379
|
+
const m = /** @type {{ requestId?: string; code?: string; timeoutMs?: number }} */ (message);
|
|
380
|
+
const requestId = m.requestId || `poke-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
381
|
+
const code = String(m.code ?? "");
|
|
382
|
+
let finished = false;
|
|
383
|
+
const timeoutMs = typeof m.timeoutMs === "number" ? m.timeoutMs : 30000;
|
|
384
|
+
|
|
385
|
+
const timer = setTimeout(() => {
|
|
386
|
+
if (finished) return;
|
|
387
|
+
finished = true;
|
|
388
|
+
window.removeEventListener("message", onWindowMessage);
|
|
389
|
+
sendResponse({ ok: false, error: "evaluate_js timed out in content script" });
|
|
390
|
+
}, timeoutMs);
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* @param {MessageEvent} event
|
|
394
|
+
*/
|
|
395
|
+
function onWindowMessage(event) {
|
|
396
|
+
if (event.source !== window) return;
|
|
397
|
+
const data = event.data;
|
|
398
|
+
if (!data || data.type !== "POKE_EVAL_RESULT" || data.requestId !== requestId) return;
|
|
399
|
+
if (finished) return;
|
|
400
|
+
finished = true;
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
window.removeEventListener("message", onWindowMessage);
|
|
403
|
+
if (data.ok) {
|
|
404
|
+
sendResponse({ ok: true, result: data.result });
|
|
405
|
+
} else {
|
|
406
|
+
sendResponse({ ok: false, error: data.error || "evaluate failed" });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
window.addEventListener("message", onWindowMessage);
|
|
411
|
+
|
|
412
|
+
const s = document.createElement("script");
|
|
413
|
+
s.textContent = `
|
|
414
|
+
(function () {
|
|
415
|
+
var requestId = ${JSON.stringify(requestId)};
|
|
416
|
+
try {
|
|
417
|
+
var result = (0, eval)(${JSON.stringify(code)});
|
|
418
|
+
window.postMessage({ type: "POKE_EVAL_RESULT", requestId: requestId, ok: true, result: result }, "*");
|
|
419
|
+
} catch (e) {
|
|
420
|
+
window.postMessage({ type: "POKE_EVAL_RESULT", requestId: requestId, ok: false, error: String(e) }, "*");
|
|
421
|
+
}
|
|
422
|
+
})();
|
|
423
|
+
`;
|
|
424
|
+
(document.documentElement || document.head || document.body).appendChild(s);
|
|
425
|
+
s.remove();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// --- Perception: shared helpers -------------------------------------------------
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @param {Record<string, unknown>} obj
|
|
432
|
+
*/
|
|
433
|
+
function compactJson(obj) {
|
|
434
|
+
return JSON.parse(JSON.stringify(obj));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* @param {Element} el
|
|
439
|
+
*/
|
|
440
|
+
function cssEscapeId(id) {
|
|
441
|
+
if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(id);
|
|
442
|
+
return id.replace(/([^\w-])/g, "\\$1");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @param {Element} el
|
|
447
|
+
*/
|
|
448
|
+
function uniqueSelector(el) {
|
|
449
|
+
if (!(el instanceof Element)) return "";
|
|
450
|
+
if (el.id && deepQueryAll(document, `#${cssEscapeId(el.id)}`).length === 1) {
|
|
451
|
+
return `#${cssEscapeId(el.id)}`;
|
|
452
|
+
}
|
|
453
|
+
const parts = [];
|
|
454
|
+
let cur = el;
|
|
455
|
+
while (cur && cur.nodeType === Node.ELEMENT_NODE && cur !== document.documentElement) {
|
|
456
|
+
let part = cur.tagName.toLowerCase();
|
|
457
|
+
if (cur.id) {
|
|
458
|
+
parts.unshift(`#${cssEscapeId(cur.id)}`);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
const parent = cur.parentElement;
|
|
462
|
+
if (parent) {
|
|
463
|
+
const siblings = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
|
|
464
|
+
const idx = siblings.indexOf(cur) + 1;
|
|
465
|
+
if (siblings.length > 1) part += `:nth-of-type(${idx})`;
|
|
466
|
+
}
|
|
467
|
+
parts.unshift(part);
|
|
468
|
+
cur = /** @type {Element} */ (parent);
|
|
469
|
+
}
|
|
470
|
+
return parts.join(" > ");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* @param {Element} el
|
|
475
|
+
*/
|
|
476
|
+
function elementInteractive(el) {
|
|
477
|
+
if (!(el instanceof Element)) return false;
|
|
478
|
+
const tag = el.tagName.toLowerCase();
|
|
479
|
+
if (["a", "button", "input", "select", "textarea", "summary", "option", "label"].includes(tag)) {
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
const role = el.getAttribute("role");
|
|
483
|
+
if (
|
|
484
|
+
role &&
|
|
485
|
+
["button", "link", "menuitem", "tab", "checkbox", "radio", "switch", "textbox", "searchbox", "combobox", "slider", "spinbutton"].includes(
|
|
486
|
+
role
|
|
487
|
+
)
|
|
488
|
+
) {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
if (el.hasAttribute("onclick")) return true;
|
|
492
|
+
if (el instanceof HTMLElement && el.isContentEditable) return true;
|
|
493
|
+
const tab = el.getAttribute("tabindex");
|
|
494
|
+
if (tab !== null && tab !== "-1" && !Number.isNaN(Number.parseInt(tab, 10))) return true;
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* @param {Element} el
|
|
500
|
+
* @param {boolean} includeHidden
|
|
501
|
+
*/
|
|
502
|
+
function isSkippedHidden(el, includeHidden) {
|
|
503
|
+
if (includeHidden) return false;
|
|
504
|
+
if (!(el instanceof HTMLElement)) return true;
|
|
505
|
+
if (el === document.body || el === document.documentElement) return false;
|
|
506
|
+
const st = window.getComputedStyle(el);
|
|
507
|
+
if (st.display === "none" || st.visibility === "hidden") return true;
|
|
508
|
+
if (el.offsetParent === null) {
|
|
509
|
+
const pos = st.position;
|
|
510
|
+
if (pos !== "fixed" && pos !== "sticky") return true;
|
|
511
|
+
}
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @param {Element} el
|
|
517
|
+
* @param {number} maxLen
|
|
518
|
+
*/
|
|
519
|
+
function trimText(el, maxLen) {
|
|
520
|
+
let t = (el.textContent || "").trim().replace(/\s+/g, " ");
|
|
521
|
+
if (t.length > maxLen) t = t.slice(0, maxLen);
|
|
522
|
+
return t;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* @param {Element} el
|
|
527
|
+
* @param {number} depth
|
|
528
|
+
* @param {number} maxDepth
|
|
529
|
+
* @param {boolean} includeHidden
|
|
530
|
+
* @param {boolean} [inShadow]
|
|
531
|
+
*/
|
|
532
|
+
function buildDomSnapshotNode(el, depth, maxDepth, includeHidden, inShadow) {
|
|
533
|
+
if (depth > maxDepth) return null;
|
|
534
|
+
if (isSkippedHidden(el, includeHidden)) return null;
|
|
535
|
+
const r = el.getBoundingClientRect();
|
|
536
|
+
/** @type {Record<string, unknown>} */
|
|
537
|
+
const node = {
|
|
538
|
+
tag: el.tagName.toLowerCase(),
|
|
539
|
+
rect: { x: r.x, y: r.y, width: r.width, height: r.height },
|
|
540
|
+
interactive: elementInteractive(el),
|
|
541
|
+
};
|
|
542
|
+
if (inShadow) node.isShadow = true;
|
|
543
|
+
if (el.id) node.id = el.id;
|
|
544
|
+
const cls =
|
|
545
|
+
typeof el.className === "string" && el.className.trim()
|
|
546
|
+
? el.className.trim().split(/\s+/).filter(Boolean)
|
|
547
|
+
: [];
|
|
548
|
+
if (cls.length) node.classes = cls;
|
|
549
|
+
const role = el.getAttribute("role");
|
|
550
|
+
if (role) node.role = role;
|
|
551
|
+
const al = el.getAttribute("aria-label");
|
|
552
|
+
if (al) node["aria-label"] = al;
|
|
553
|
+
const tx = trimText(el, 120);
|
|
554
|
+
if (tx) node.text = tx;
|
|
555
|
+
const childEls = Array.from(el.children);
|
|
556
|
+
const children = [];
|
|
557
|
+
for (const c of childEls) {
|
|
558
|
+
const sn = buildDomSnapshotNode(c, depth + 1, maxDepth, includeHidden, inShadow);
|
|
559
|
+
if (sn) children.push(sn);
|
|
560
|
+
}
|
|
561
|
+
if (el.shadowRoot) {
|
|
562
|
+
for (const c of Array.from(el.shadowRoot.children)) {
|
|
563
|
+
const sn = buildDomSnapshotNode(c, depth + 1, maxDepth, includeHidden, true);
|
|
564
|
+
if (sn) children.push(sn);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (children.length) node.children = children;
|
|
568
|
+
return node;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* @param {unknown} message
|
|
573
|
+
* @param {(r: unknown) => void} sendResponse
|
|
574
|
+
*/
|
|
575
|
+
function handleGetDomSnapshot(message, sendResponse) {
|
|
576
|
+
const m = /** @type {{ includeHidden?: boolean; maxDepth?: number }} */ (message);
|
|
577
|
+
const includeHidden = m.includeHidden === true;
|
|
578
|
+
const maxDepth = typeof m.maxDepth === "number" && Number.isFinite(m.maxDepth) ? Math.max(0, Math.min(50, m.maxDepth)) : 6;
|
|
579
|
+
if (!document.body) {
|
|
580
|
+
sendResponse({ error: "No document.body" });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const snapshot = buildDomSnapshotNode(document.body, 0, maxDepth, includeHidden);
|
|
584
|
+
sendResponse(
|
|
585
|
+
compactJson({
|
|
586
|
+
snapshot,
|
|
587
|
+
url: location.href,
|
|
588
|
+
title: document.title || "",
|
|
589
|
+
timestamp: Date.now(),
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* @param {Element} el
|
|
596
|
+
*/
|
|
597
|
+
function impliedRole(el) {
|
|
598
|
+
const r = el.getAttribute("role");
|
|
599
|
+
if (r) return r;
|
|
600
|
+
const t = el.tagName.toLowerCase();
|
|
601
|
+
if (t === "a") return "link";
|
|
602
|
+
if (t === "button") return "button";
|
|
603
|
+
if (t === "select") return "combobox";
|
|
604
|
+
if (t === "textarea") return "textbox";
|
|
605
|
+
if (t === "img") return "img";
|
|
606
|
+
if (t === "form") return "form";
|
|
607
|
+
if (t === "input") {
|
|
608
|
+
const type = (/** @type {HTMLInputElement} */ (el)).type || "text";
|
|
609
|
+
if (type === "checkbox") return "checkbox";
|
|
610
|
+
if (type === "radio") return "radio";
|
|
611
|
+
if (type === "button" || type === "submit" || type === "reset") return "button";
|
|
612
|
+
return "textbox";
|
|
613
|
+
}
|
|
614
|
+
if (/^h[1-6]$/.test(t)) return "heading";
|
|
615
|
+
if (t === "p") return "paragraph";
|
|
616
|
+
if (t === "li") return "listitem";
|
|
617
|
+
return t;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* @param {Element} el
|
|
622
|
+
*/
|
|
623
|
+
function accessibilityName(el) {
|
|
624
|
+
const aria = el.getAttribute("aria-label");
|
|
625
|
+
if (aria && aria.trim()) return aria.trim().slice(0, 80);
|
|
626
|
+
if (el instanceof HTMLImageElement && el.alt) return el.alt.trim().slice(0, 80);
|
|
627
|
+
const title = el.getAttribute("title");
|
|
628
|
+
if (title && title.trim()) return title.trim().slice(0, 80);
|
|
629
|
+
const ph = el.getAttribute("aria-placeholder");
|
|
630
|
+
if (ph && ph.trim()) return ph.trim().slice(0, 80);
|
|
631
|
+
const it = (el.innerText || "").trim().replace(/\s+/g, " ");
|
|
632
|
+
return it.length > 80 ? it.slice(0, 80) : it;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* @param {Element} el
|
|
637
|
+
*/
|
|
638
|
+
function isFocusableInteractive(el) {
|
|
639
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
640
|
+
if (el.hasAttribute("disabled")) return false;
|
|
641
|
+
if (elementInteractive(el)) {
|
|
642
|
+
const tab = el.getAttribute("tabindex");
|
|
643
|
+
if (tab === "-1" && !["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA", "SUMMARY"].includes(el.tagName)) {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* @param {unknown} message
|
|
653
|
+
* @param {(r: unknown) => void} sendResponse
|
|
654
|
+
*/
|
|
655
|
+
function handleGetAccessibilityTree(message, sendResponse) {
|
|
656
|
+
const m = /** @type {{ interactiveOnly?: boolean }} */ (message);
|
|
657
|
+
const interactiveOnly = m.interactiveOnly === true;
|
|
658
|
+
const sel =
|
|
659
|
+
'[role], a, button, input, select, textarea, h1, h2, h3, h4, h5, h6, p, li, img, form';
|
|
660
|
+
const list = Array.from(document.querySelectorAll(sel));
|
|
661
|
+
/** @type {Array<Record<string, unknown>>} */
|
|
662
|
+
const raw = [];
|
|
663
|
+
for (const el of list) {
|
|
664
|
+
if (!(el instanceof Element)) continue;
|
|
665
|
+
if (isSkippedHidden(el, false)) continue;
|
|
666
|
+
if (interactiveOnly && !isFocusableInteractive(el)) continue;
|
|
667
|
+
const r = el.getBoundingClientRect();
|
|
668
|
+
const tag = el.tagName.toLowerCase();
|
|
669
|
+
/** @type {Record<string, unknown>} */
|
|
670
|
+
const row = {
|
|
671
|
+
role: impliedRole(el),
|
|
672
|
+
name: accessibilityName(el),
|
|
673
|
+
selector: uniqueSelector(el),
|
|
674
|
+
disabled: el instanceof HTMLElement && (el.hasAttribute("disabled") || /** @type {HTMLInputElement} */ (el).disabled === true),
|
|
675
|
+
rect: { x: r.x, y: r.y, w: r.width, h: r.height },
|
|
676
|
+
};
|
|
677
|
+
if (el.id) row.id = el.id;
|
|
678
|
+
if (/^h[1-6]$/.test(tag)) row.level = Number(tag[1]);
|
|
679
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
680
|
+
row.value = el.value;
|
|
681
|
+
if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) {
|
|
682
|
+
row.checked = el.checked;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
raw.push(row);
|
|
686
|
+
}
|
|
687
|
+
raw.sort((a, b) => {
|
|
688
|
+
const ra = /** @type {{ x: number; y: number }} */ (a.rect);
|
|
689
|
+
const rb = /** @type {{ x: number; y: number }} */ (b.rect);
|
|
690
|
+
if (Math.abs(ra.y - rb.y) > 1) return ra.y - rb.y;
|
|
691
|
+
return ra.x - rb.x;
|
|
692
|
+
});
|
|
693
|
+
const nodes = raw.map((row) => compactJson({ ...row }));
|
|
694
|
+
sendResponse({
|
|
695
|
+
nodes,
|
|
696
|
+
count: nodes.length,
|
|
697
|
+
url: location.href,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* @param {string} expr
|
|
703
|
+
* @returns {Element[]}
|
|
704
|
+
*/
|
|
705
|
+
function xpathElements(expr) {
|
|
706
|
+
return deepXPathAll(expr);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* @param {string} q
|
|
711
|
+
* @returns {Element[]}
|
|
712
|
+
*/
|
|
713
|
+
function findElementsByText(q) {
|
|
714
|
+
const ql = q.toLowerCase().trim();
|
|
715
|
+
if (!ql) return [];
|
|
716
|
+
const all = deepQueryAll(document, "*");
|
|
717
|
+
/** @type {Element[]} */
|
|
718
|
+
const exact = [];
|
|
719
|
+
/** @type {Element[]} */
|
|
720
|
+
const partial = [];
|
|
721
|
+
for (const el of all) {
|
|
722
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
723
|
+
const tn = el.tagName;
|
|
724
|
+
if (tn === "SCRIPT" || tn === "STYLE" || tn === "NOSCRIPT") continue;
|
|
725
|
+
const t = (el.innerText || "").trim();
|
|
726
|
+
if (!t) continue;
|
|
727
|
+
const tl = t.toLowerCase();
|
|
728
|
+
if (tl === ql) exact.push(el);
|
|
729
|
+
else if (tl.includes(ql)) partial.push(el);
|
|
730
|
+
}
|
|
731
|
+
const pool = exact.length ? exact : partial;
|
|
732
|
+
return filterOutAncestors(pool);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* @param {Element[]} els
|
|
737
|
+
*/
|
|
738
|
+
function filterOutAncestors(els) {
|
|
739
|
+
/** @type {Element[]} */
|
|
740
|
+
const out = [];
|
|
741
|
+
for (const el of els) {
|
|
742
|
+
let sub = false;
|
|
743
|
+
for (const o of els) {
|
|
744
|
+
if (o !== el && o.contains(el)) {
|
|
745
|
+
sub = true;
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (!sub) out.push(el);
|
|
750
|
+
}
|
|
751
|
+
return out;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* @param {string} q
|
|
756
|
+
* @returns {Element[]}
|
|
757
|
+
*/
|
|
758
|
+
function findElementsByAria(q) {
|
|
759
|
+
const ql = q.toLowerCase().trim();
|
|
760
|
+
if (!ql) return [];
|
|
761
|
+
const all = deepQueryAll(document, "*");
|
|
762
|
+
/** @type {Element[]} */
|
|
763
|
+
const hits = [];
|
|
764
|
+
for (const el of all) {
|
|
765
|
+
if (!(el instanceof Element)) continue;
|
|
766
|
+
const tn = el.tagName;
|
|
767
|
+
if (tn === "SCRIPT" || tn === "STYLE" || tn === "NOSCRIPT") continue;
|
|
768
|
+
const chunks = [
|
|
769
|
+
el.getAttribute("aria-label"),
|
|
770
|
+
el.getAttribute("aria-placeholder"),
|
|
771
|
+
el.getAttribute("title"),
|
|
772
|
+
el instanceof HTMLImageElement ? el.alt : null,
|
|
773
|
+
]
|
|
774
|
+
.filter(Boolean)
|
|
775
|
+
.map((s) => String(s).toLowerCase());
|
|
776
|
+
if (chunks.some((c) => c.includes(ql))) hits.push(el);
|
|
777
|
+
}
|
|
778
|
+
return filterOutAncestors(hits);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* @param {Element} el
|
|
783
|
+
* @param {number} index
|
|
784
|
+
*/
|
|
785
|
+
function toFoundElement(el, index) {
|
|
786
|
+
const r = el.getBoundingClientRect();
|
|
787
|
+
const cls =
|
|
788
|
+
typeof el.className === "string" && el.className.trim()
|
|
789
|
+
? el.className.trim().split(/\s+/).filter(Boolean)
|
|
790
|
+
: [];
|
|
791
|
+
/** @type {Record<string, unknown>} */
|
|
792
|
+
const o = {
|
|
793
|
+
index,
|
|
794
|
+
tag: el.tagName.toLowerCase(),
|
|
795
|
+
text: (el.innerText || "").trim().slice(0, 200),
|
|
796
|
+
selector: uniqueSelector(el),
|
|
797
|
+
rect: { x: r.x, y: r.y, width: r.width, height: r.height },
|
|
798
|
+
interactive: elementInteractive(el),
|
|
799
|
+
};
|
|
800
|
+
if (el.id) o.id = el.id;
|
|
801
|
+
if (cls.length) o.classes = cls;
|
|
802
|
+
return compactJson(o);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* @param {unknown} message
|
|
807
|
+
* @param {(r: unknown) => void} sendResponse
|
|
808
|
+
*/
|
|
809
|
+
function handleFindElement(message, sendResponse) {
|
|
810
|
+
const m = /** @type {{ query?: string; strategy?: string }} */ (message);
|
|
811
|
+
const query = typeof m.query === "string" ? m.query : "";
|
|
812
|
+
const strategy = m.strategy === "css" || m.strategy === "text" || m.strategy === "aria" || m.strategy === "xpath" ? m.strategy : "auto";
|
|
813
|
+
if (!query.trim()) {
|
|
814
|
+
sendResponse({ elements: [], query: "", strategy_used: strategy });
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** @type {Element[]} */
|
|
819
|
+
let found = [];
|
|
820
|
+
/** @type {string} */
|
|
821
|
+
let used = strategy;
|
|
822
|
+
|
|
823
|
+
function tryCss() {
|
|
824
|
+
try {
|
|
825
|
+
return deepQueryAll(document, query);
|
|
826
|
+
} catch {
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (strategy === "auto") {
|
|
832
|
+
found = tryCss();
|
|
833
|
+
used = "css";
|
|
834
|
+
if (found.length === 0) {
|
|
835
|
+
found = findElementsByText(query);
|
|
836
|
+
used = "text";
|
|
837
|
+
}
|
|
838
|
+
if (found.length === 0) {
|
|
839
|
+
found = findElementsByAria(query);
|
|
840
|
+
used = "aria";
|
|
841
|
+
}
|
|
842
|
+
} else if (strategy === "css") {
|
|
843
|
+
found = tryCss();
|
|
844
|
+
} else if (strategy === "text") {
|
|
845
|
+
found = findElementsByText(query);
|
|
846
|
+
} else if (strategy === "aria") {
|
|
847
|
+
found = findElementsByAria(query);
|
|
848
|
+
} else if (strategy === "xpath") {
|
|
849
|
+
found = xpathElements(query);
|
|
850
|
+
used = "xpath";
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const top = found.slice(0, 5);
|
|
854
|
+
const elements = top.map((el, i) => toFoundElement(el, i));
|
|
855
|
+
sendResponse({ elements, query, strategy_used: used });
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* @returns {HTMLElement}
|
|
860
|
+
*/
|
|
861
|
+
function getReadPageRoot() {
|
|
862
|
+
const main =
|
|
863
|
+
document.querySelector("main") ||
|
|
864
|
+
document.querySelector("article") ||
|
|
865
|
+
document.querySelector('[role="main"]');
|
|
866
|
+
if (main instanceof HTMLElement) return main;
|
|
867
|
+
return document.body || document.documentElement;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* @param {HTMLElement} el
|
|
872
|
+
*/
|
|
873
|
+
function stripNoise(el) {
|
|
874
|
+
el.querySelectorAll("script, style, noscript, nav, header, footer").forEach((n) => n.remove());
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* @param {string} text
|
|
879
|
+
*/
|
|
880
|
+
function wordCountFrom(text) {
|
|
881
|
+
const w = text.trim().split(/\s+/).filter(Boolean);
|
|
882
|
+
return w.length;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* @param {HTMLElement} root
|
|
887
|
+
*/
|
|
888
|
+
function readStructured(root) {
|
|
889
|
+
const clone = /** @type {HTMLElement} */ (root.cloneNode(true));
|
|
890
|
+
stripNoise(clone);
|
|
891
|
+
const descMeta = document.querySelector('meta[name="description"]');
|
|
892
|
+
const description = descMeta?.getAttribute("content")?.trim() || "";
|
|
893
|
+
/** @type {{ level: number; text: string }[]} */
|
|
894
|
+
const headings = [];
|
|
895
|
+
clone.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
|
|
896
|
+
const tag = h.tagName.toLowerCase();
|
|
897
|
+
headings.push({ level: Number(tag[1]), text: (h.textContent || "").trim() });
|
|
898
|
+
});
|
|
899
|
+
/** @type {{ text: string; href: string }[]} */
|
|
900
|
+
const links = [];
|
|
901
|
+
clone.querySelectorAll("a[href]").forEach((a) => {
|
|
902
|
+
const href = a.getAttribute("href") || "";
|
|
903
|
+
links.push({ text: (a.textContent || "").trim(), href });
|
|
904
|
+
});
|
|
905
|
+
/** @type {{ alt: string; src: string }[]} */
|
|
906
|
+
const images = [];
|
|
907
|
+
clone.querySelectorAll("img[src]").forEach((img) => {
|
|
908
|
+
images.push({ alt: img.getAttribute("alt") || "", src: img.getAttribute("src") || "" });
|
|
909
|
+
});
|
|
910
|
+
const mainText = (clone.innerText || "").trim().replace(/\s+/g, " ");
|
|
911
|
+
return {
|
|
912
|
+
title: document.title || "",
|
|
913
|
+
url: location.href,
|
|
914
|
+
description,
|
|
915
|
+
mainText,
|
|
916
|
+
headings,
|
|
917
|
+
links,
|
|
918
|
+
images,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* @param {HTMLElement} el
|
|
924
|
+
* @returns {string}
|
|
925
|
+
*/
|
|
926
|
+
function elementToMarkdown(el) {
|
|
927
|
+
const tag = el.tagName.toLowerCase();
|
|
928
|
+
if (["script", "style", "noscript", "nav", "header", "footer"].includes(tag)) return "";
|
|
929
|
+
if (tag === "br") return "\n";
|
|
930
|
+
if (el.childNodes.length === 0) return "";
|
|
931
|
+
|
|
932
|
+
/** @type {string[]} */
|
|
933
|
+
const bits = [];
|
|
934
|
+
for (const node of el.childNodes) {
|
|
935
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
936
|
+
const t = node.textContent || "";
|
|
937
|
+
if (t.trim()) bits.push(t);
|
|
938
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
939
|
+
const child = /** @type {HTMLElement} */ (node);
|
|
940
|
+
const ct = child.tagName.toLowerCase();
|
|
941
|
+
if (["script", "style", "noscript", "nav", "header", "footer"].includes(ct)) continue;
|
|
942
|
+
if (/^h[1-6]$/.test(ct)) {
|
|
943
|
+
const level = Number(ct[1]);
|
|
944
|
+
bits.push(`${"#".repeat(level)} ${(child.innerText || "").trim()}\n\n`);
|
|
945
|
+
} else if (ct === "p") {
|
|
946
|
+
bits.push(`${(child.innerText || "").trim()}\n\n`);
|
|
947
|
+
} else if (ct === "a" && child.getAttribute("href")) {
|
|
948
|
+
const href = child.getAttribute("href") || "";
|
|
949
|
+
bits.push(`[${(child.textContent || "").trim()}](${href})`);
|
|
950
|
+
} else if (ct === "ul") {
|
|
951
|
+
for (const li of child.querySelectorAll(":scope > li")) {
|
|
952
|
+
bits.push(`- ${(li.textContent || "").trim()}\n`);
|
|
953
|
+
}
|
|
954
|
+
bits.push("\n");
|
|
955
|
+
} else if (ct === "ol") {
|
|
956
|
+
let i = 1;
|
|
957
|
+
for (const li of child.querySelectorAll(":scope > li")) {
|
|
958
|
+
bits.push(`${i}. ${(li.textContent || "").trim()}\n`);
|
|
959
|
+
i += 1;
|
|
960
|
+
}
|
|
961
|
+
bits.push("\n");
|
|
962
|
+
} else if (ct === "pre") {
|
|
963
|
+
bits.push(`\`\`\`\n${(child.textContent || "").trim()}\n\`\`\`\n\n`);
|
|
964
|
+
} else if (ct === "code" && child.parentElement?.tagName.toLowerCase() !== "pre") {
|
|
965
|
+
bits.push(`\`${(child.textContent || "").trim()}\``);
|
|
966
|
+
} else if (ct === "strong" || ct === "b") {
|
|
967
|
+
bits.push(`**${(child.textContent || "").trim()}**`);
|
|
968
|
+
} else if (ct === "img" && child.getAttribute("src")) {
|
|
969
|
+
const src = child.getAttribute("src") || "";
|
|
970
|
+
const alt = child.getAttribute("alt") || "";
|
|
971
|
+
bits.push(``);
|
|
972
|
+
} else {
|
|
973
|
+
bits.push(elementToMarkdown(child));
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return bits.join("");
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* @param {unknown} message
|
|
982
|
+
* @param {(r: unknown) => void} sendResponse
|
|
983
|
+
*/
|
|
984
|
+
/**
|
|
985
|
+
* @param {Element} el
|
|
986
|
+
* @param {boolean} requireVisible
|
|
987
|
+
*/
|
|
988
|
+
function elementMatchesVisible(el, requireVisible) {
|
|
989
|
+
if (!requireVisible) return true;
|
|
990
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
991
|
+
if (el.offsetParent === null) {
|
|
992
|
+
const st = getComputedStyle(el);
|
|
993
|
+
const pos = st.position;
|
|
994
|
+
if (pos !== "fixed" && pos !== "sticky") return false;
|
|
995
|
+
}
|
|
996
|
+
const st = getComputedStyle(el);
|
|
997
|
+
if (st.display === "none" || st.visibility === "hidden" || Number.parseFloat(st.opacity) === 0) {
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* @param {unknown} message
|
|
1005
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1006
|
+
*/
|
|
1007
|
+
function handleWaitForSelector(message, sendResponse) {
|
|
1008
|
+
const m = /** @type {{ selector?: string; timeout?: number; visible?: boolean }} */ (message);
|
|
1009
|
+
const selector = typeof m.selector === "string" ? m.selector : "";
|
|
1010
|
+
const timeout = typeof m.timeout === "number" && m.timeout > 0 ? m.timeout : 10000;
|
|
1011
|
+
const visible = m.visible === true;
|
|
1012
|
+
const start = Date.now();
|
|
1013
|
+
|
|
1014
|
+
/** @type {ReturnType<typeof setInterval> | undefined} */
|
|
1015
|
+
let iv;
|
|
1016
|
+
|
|
1017
|
+
function tick() {
|
|
1018
|
+
const el = querySelectorOrXPath(selector);
|
|
1019
|
+
if (el && elementMatchesVisible(el, visible)) {
|
|
1020
|
+
if (iv !== undefined) clearInterval(iv);
|
|
1021
|
+
const r = el.getBoundingClientRect();
|
|
1022
|
+
sendResponse({
|
|
1023
|
+
found: true,
|
|
1024
|
+
selector,
|
|
1025
|
+
elapsed: Date.now() - start,
|
|
1026
|
+
element: {
|
|
1027
|
+
tag: el.tagName.toLowerCase(),
|
|
1028
|
+
id: el.id || undefined,
|
|
1029
|
+
text: (el.textContent || "").trim().slice(0, 200),
|
|
1030
|
+
rect: { x: r.x, y: r.y, width: r.width, height: r.height },
|
|
1031
|
+
},
|
|
1032
|
+
});
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (Date.now() - start >= timeout) {
|
|
1036
|
+
if (iv !== undefined) clearInterval(iv);
|
|
1037
|
+
sendResponse({
|
|
1038
|
+
found: false,
|
|
1039
|
+
selector,
|
|
1040
|
+
elapsed: Date.now() - start,
|
|
1041
|
+
error: "timeout",
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
iv = setInterval(tick, 100);
|
|
1047
|
+
tick();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* @param {unknown} message
|
|
1052
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1053
|
+
*/
|
|
1054
|
+
function handleGetConsoleLogs(message, sendResponse) {
|
|
1055
|
+
const m = /** @type {{ level?: string; limit?: number }} */ (message);
|
|
1056
|
+
const level = m.level === "error" || m.level === "warn" || m.level === "info" || m.level === "log" ? m.level : "all";
|
|
1057
|
+
const limit = typeof m.limit === "number" ? Math.min(500, Math.max(1, m.limit)) : 100;
|
|
1058
|
+
let logs = consoleRing;
|
|
1059
|
+
if (level !== "all") {
|
|
1060
|
+
logs = logs.filter((e) => e.level === level);
|
|
1061
|
+
}
|
|
1062
|
+
const sliced = logs.slice(-limit);
|
|
1063
|
+
sendResponse({ logs: sliced, count: sliced.length });
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* @param {unknown} _message
|
|
1068
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1069
|
+
*/
|
|
1070
|
+
function handleClearConsoleLogs(_message, sendResponse) {
|
|
1071
|
+
consoleRing = [];
|
|
1072
|
+
sendResponse({ cleared: true });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* @param {unknown} message
|
|
1077
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1078
|
+
*/
|
|
1079
|
+
function handleGetPageErrors(message, sendResponse) {
|
|
1080
|
+
const m = /** @type {{ limit?: number }} */ (message);
|
|
1081
|
+
const limit = typeof m.limit === "number" ? Math.min(200, Math.max(1, m.limit)) : 50;
|
|
1082
|
+
const sliced = pageErrorRing.slice(-limit);
|
|
1083
|
+
sendResponse({ errors: sliced, count: sliced.length });
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* @param {unknown} _message
|
|
1088
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1089
|
+
*/
|
|
1090
|
+
function handleGetScrollInfo(_message, sendResponse) {
|
|
1091
|
+
const de = document.documentElement;
|
|
1092
|
+
const body = document.body;
|
|
1093
|
+
sendResponse({
|
|
1094
|
+
scrollHeight: Math.max(de.scrollHeight, body ? body.scrollHeight : 0, de.clientHeight),
|
|
1095
|
+
innerHeight: window.innerHeight,
|
|
1096
|
+
innerWidth: window.innerWidth,
|
|
1097
|
+
scrollY: window.scrollY,
|
|
1098
|
+
devicePixelRatio: window.devicePixelRatio || 1,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* @param {unknown} message
|
|
1104
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1105
|
+
*/
|
|
1106
|
+
function handleScrollTo(message, sendResponse) {
|
|
1107
|
+
const m = /** @type {{ y?: number }} */ (message);
|
|
1108
|
+
const y = typeof m.y === "number" ? m.y : 0;
|
|
1109
|
+
window.scrollTo({ top: y, left: 0, behavior: "instant" });
|
|
1110
|
+
sendResponse({ scrollY: window.scrollY });
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* @param {unknown} message
|
|
1115
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1116
|
+
*/
|
|
1117
|
+
function handleHoverElement(message, sendResponse) {
|
|
1118
|
+
const m = /** @type {{ selector?: string }} */ (message);
|
|
1119
|
+
const selector = typeof m.selector === "string" ? m.selector : "";
|
|
1120
|
+
if (!selector.trim()) {
|
|
1121
|
+
sendResponse({ success: false });
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const el = querySelectorOrXPath(selector);
|
|
1125
|
+
if (!el) {
|
|
1126
|
+
sendResponse({ success: false });
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const r = el.getBoundingClientRect();
|
|
1130
|
+
const x = r.left + r.width / 2;
|
|
1131
|
+
const y = r.top + r.height / 2;
|
|
1132
|
+
const init = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
|
|
1133
|
+
el.dispatchEvent(new MouseEvent("mousemove", init));
|
|
1134
|
+
el.dispatchEvent(new MouseEvent("mouseover", init));
|
|
1135
|
+
el.dispatchEvent(new MouseEvent("mouseenter", init));
|
|
1136
|
+
sendResponse({
|
|
1137
|
+
success: true,
|
|
1138
|
+
element: {
|
|
1139
|
+
tag: el.tagName.toLowerCase(),
|
|
1140
|
+
id: el.id || undefined,
|
|
1141
|
+
text: (el.textContent || "").trim().slice(0, 200),
|
|
1142
|
+
rect: { x: r.x, y: r.y, width: r.width, height: r.height },
|
|
1143
|
+
},
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* @param {unknown} message
|
|
1149
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1150
|
+
*/
|
|
1151
|
+
function handleScriptInject(message, sendResponse) {
|
|
1152
|
+
const m = /** @type {{ script?: string }} */ (message);
|
|
1153
|
+
const script = typeof m.script === "string" ? m.script : "";
|
|
1154
|
+
if (!script) {
|
|
1155
|
+
sendResponse({ success: false });
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
try {
|
|
1159
|
+
const s = document.createElement("script");
|
|
1160
|
+
s.textContent = script;
|
|
1161
|
+
const root = document.documentElement || document.head || document.body;
|
|
1162
|
+
if (!root) {
|
|
1163
|
+
sendResponse({ success: false });
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
root.appendChild(s);
|
|
1167
|
+
s.remove();
|
|
1168
|
+
sendResponse({ success: true });
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
sendResponse({ success: false, error: String(err) });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* @param {unknown} message
|
|
1176
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1177
|
+
*/
|
|
1178
|
+
function handleFillForm(message, sendResponse) {
|
|
1179
|
+
const m = /** @type {{
|
|
1180
|
+
fields?: Array<{ selector?: string; value?: string; type?: string }>;
|
|
1181
|
+
submitAfter?: boolean;
|
|
1182
|
+
submitSelector?: string;
|
|
1183
|
+
}} */ (message);
|
|
1184
|
+
const fields = Array.isArray(m.fields) ? m.fields : [];
|
|
1185
|
+
/** @type {Array<{ selector: string; error: string }>} */
|
|
1186
|
+
const errors = [];
|
|
1187
|
+
let filled = 0;
|
|
1188
|
+
|
|
1189
|
+
for (const f of fields) {
|
|
1190
|
+
const sel = typeof f.selector === "string" ? f.selector : "";
|
|
1191
|
+
const val = typeof f.value === "string" ? f.value : "";
|
|
1192
|
+
const typ = f.type === "select" || f.type === "checkbox" || f.type === "radio" || f.type === "file" ? f.type : "text";
|
|
1193
|
+
if (!sel) {
|
|
1194
|
+
errors.push({ selector: sel, error: "empty selector" });
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
const el = querySelectorOrXPath(sel);
|
|
1198
|
+
if (!el) {
|
|
1199
|
+
errors.push({ selector: sel, error: "not found" });
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
try {
|
|
1203
|
+
if (typ === "file") {
|
|
1204
|
+
errors.push({ selector: sel, error: "file inputs are not supported" });
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
if (typ === "checkbox") {
|
|
1208
|
+
const input = el instanceof HTMLInputElement ? el : null;
|
|
1209
|
+
if (!input || input.type !== "checkbox") {
|
|
1210
|
+
errors.push({ selector: sel, error: "not a checkbox input" });
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
const vl = val.toLowerCase();
|
|
1214
|
+
input.checked = !(vl === "false" || val === "0" || vl === "off" || val === "");
|
|
1215
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1216
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1217
|
+
filled += 1;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if (typ === "radio") {
|
|
1221
|
+
const input = el instanceof HTMLInputElement ? el : null;
|
|
1222
|
+
if (!input || input.type !== "radio") {
|
|
1223
|
+
errors.push({ selector: sel, error: "not a radio input" });
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const vl = val.toLowerCase();
|
|
1227
|
+
const off = val === "" || vl === "false" || val === "0" || vl === "off";
|
|
1228
|
+
if (off) {
|
|
1229
|
+
input.checked = false;
|
|
1230
|
+
} else {
|
|
1231
|
+
input.checked = true;
|
|
1232
|
+
if (input.form) {
|
|
1233
|
+
const rads = input.form.querySelectorAll('input[type="radio"]');
|
|
1234
|
+
rads.forEach((x) => {
|
|
1235
|
+
if (x instanceof HTMLInputElement && x.name === input.name && x !== input) {
|
|
1236
|
+
x.checked = false;
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1242
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1243
|
+
filled += 1;
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
if (typ === "select" && el instanceof HTMLSelectElement) {
|
|
1247
|
+
let matched = false;
|
|
1248
|
+
for (let i = 0; i < el.options.length; i += 1) {
|
|
1249
|
+
const o = el.options[i];
|
|
1250
|
+
if (o.value === val || o.text === val) {
|
|
1251
|
+
el.selectedIndex = i;
|
|
1252
|
+
matched = true;
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (!matched) el.value = val;
|
|
1257
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1258
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1259
|
+
filled += 1;
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
1263
|
+
el.focus();
|
|
1264
|
+
el.value = val;
|
|
1265
|
+
el.dispatchEvent(
|
|
1266
|
+
new InputEvent("input", { bubbles: true, data: val, inputType: "insertReplacementText" }),
|
|
1267
|
+
);
|
|
1268
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1269
|
+
filled += 1;
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
errors.push({ selector: sel, error: "unsupported element for text fill" });
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
errors.push({ selector: sel, error: String(err) });
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
let success = errors.length === 0;
|
|
1279
|
+
if (m.submitAfter === true) {
|
|
1280
|
+
const subSel = typeof m.submitSelector === "string" ? m.submitSelector.trim() : "";
|
|
1281
|
+
let sub = subSel ? querySelectorOrXPath(subSel) : null;
|
|
1282
|
+
if (!sub && fields[0]) {
|
|
1283
|
+
const firstSel = typeof fields[0].selector === "string" ? fields[0].selector : "";
|
|
1284
|
+
const first = firstSel ? querySelectorOrXPath(firstSel) : null;
|
|
1285
|
+
const form = first && first.closest ? first.closest("form") : null;
|
|
1286
|
+
if (form) {
|
|
1287
|
+
sub =
|
|
1288
|
+
form.querySelector('button[type="submit"], input[type="submit"], button:not([type])');
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (sub) {
|
|
1292
|
+
syntheticClick(sub);
|
|
1293
|
+
} else {
|
|
1294
|
+
errors.push({ selector: "[submit]", error: "no submit control found" });
|
|
1295
|
+
success = false;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
sendResponse({ success, filled, errors });
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* @param {unknown} message
|
|
1304
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1305
|
+
*/
|
|
1306
|
+
function handleGetStoragePage(message, sendResponse) {
|
|
1307
|
+
const m = /** @type {{ storageType?: string; key?: string }} */ (message);
|
|
1308
|
+
const useSession = m.storageType === "session";
|
|
1309
|
+
const t = useSession ? sessionStorage : localStorage;
|
|
1310
|
+
const key = typeof m.key === "string" ? m.key : undefined;
|
|
1311
|
+
/** @type {Record<string, string>} */
|
|
1312
|
+
const data = {};
|
|
1313
|
+
try {
|
|
1314
|
+
if (key) {
|
|
1315
|
+
const v = t.getItem(key);
|
|
1316
|
+
if (v !== null) data[key] = v;
|
|
1317
|
+
} else {
|
|
1318
|
+
for (let i = 0; i < t.length; i += 1) {
|
|
1319
|
+
const k = t.key(i);
|
|
1320
|
+
if (k) data[k] = t.getItem(k) ?? "";
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
sendResponse({ data, count: Object.keys(data).length });
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
sendResponse({ data: {}, count: 0, error: String(err) });
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* @param {unknown} message
|
|
1331
|
+
* @param {(r: unknown) => void} sendResponse
|
|
1332
|
+
*/
|
|
1333
|
+
function handleSetStoragePage(message, sendResponse) {
|
|
1334
|
+
const m = /** @type {{ storageType?: string; key?: string; value?: string }} */ (message);
|
|
1335
|
+
const useSession = m.storageType === "session";
|
|
1336
|
+
const t = useSession ? sessionStorage : localStorage;
|
|
1337
|
+
const key = typeof m.key === "string" ? m.key : "";
|
|
1338
|
+
const value = typeof m.value === "string" ? m.value : "";
|
|
1339
|
+
if (!key) {
|
|
1340
|
+
sendResponse({ success: false });
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
try {
|
|
1344
|
+
t.setItem(key, value);
|
|
1345
|
+
sendResponse({ success: true });
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
sendResponse({ success: false, error: String(err) });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function handleReadPage(message, sendResponse) {
|
|
1352
|
+
const m = /** @type {{ format?: string }} */ (message);
|
|
1353
|
+
const format =
|
|
1354
|
+
m.format === "markdown" || m.format === "text" || m.format === "structured" ? m.format : "structured";
|
|
1355
|
+
const root = getReadPageRoot();
|
|
1356
|
+
const title = document.title || "";
|
|
1357
|
+
const url = location.href;
|
|
1358
|
+
|
|
1359
|
+
if (format === "text") {
|
|
1360
|
+
const clone = /** @type {HTMLElement} */ (root.cloneNode(true));
|
|
1361
|
+
stripNoise(clone);
|
|
1362
|
+
const text = (clone.innerText || "").trim().replace(/\s+/g, " ");
|
|
1363
|
+
sendResponse({ text, url, title, wordCount: wordCountFrom(text) });
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (format === "markdown") {
|
|
1368
|
+
const clone = /** @type {HTMLElement} */ (root.cloneNode(true));
|
|
1369
|
+
stripNoise(clone);
|
|
1370
|
+
const markdown = elementToMarkdown(clone).trim();
|
|
1371
|
+
sendResponse({ markdown, url, title, wordCount: wordCountFrom(markdown) });
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const structured = readStructured(root);
|
|
1376
|
+
const wc = wordCountFrom(structured.mainText);
|
|
1377
|
+
sendResponse({ ...structured, wordCount: wc });
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/** @type {Record<string, (message: unknown, sendResponse: (r: unknown) => void) => void>} */
|
|
1381
|
+
const MESSAGE_HANDLERS = {
|
|
1382
|
+
POKE_CLICK_ELEMENT: handleClickElement,
|
|
1383
|
+
POKE_RESOLVE_CLICK_POINT: handleResolveClickPoint,
|
|
1384
|
+
POKE_TYPE_TEXT: handleTypeText,
|
|
1385
|
+
POKE_SCROLL_WINDOW: handleScrollWindow,
|
|
1386
|
+
POKE_EVAL: handleEval,
|
|
1387
|
+
POKE_GET_DOM_SNAPSHOT: handleGetDomSnapshot,
|
|
1388
|
+
POKE_GET_A11Y_TREE: handleGetAccessibilityTree,
|
|
1389
|
+
POKE_FIND_ELEMENT: handleFindElement,
|
|
1390
|
+
POKE_READ_PAGE: handleReadPage,
|
|
1391
|
+
POKE_WAIT_FOR_SELECTOR: handleWaitForSelector,
|
|
1392
|
+
POKE_GET_CONSOLE_LOGS: handleGetConsoleLogs,
|
|
1393
|
+
POKE_CLEAR_CONSOLE_LOGS: handleClearConsoleLogs,
|
|
1394
|
+
POKE_GET_PAGE_ERRORS: handleGetPageErrors,
|
|
1395
|
+
POKE_GET_SCROLL_INFO: handleGetScrollInfo,
|
|
1396
|
+
POKE_SCROLL_TO: handleScrollTo,
|
|
1397
|
+
POKE_HOVER_ELEMENT: handleHoverElement,
|
|
1398
|
+
POKE_SCRIPT_INJECT: handleScriptInject,
|
|
1399
|
+
POKE_FILL_FORM: handleFillForm,
|
|
1400
|
+
POKE_GET_STORAGE: handleGetStoragePage,
|
|
1401
|
+
POKE_SET_STORAGE: handleSetStoragePage,
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
1405
|
+
const t = message && typeof message === "object" && "type" in message ? String(message.type) : "";
|
|
1406
|
+
const fn = MESSAGE_HANDLERS[t];
|
|
1407
|
+
if (!fn) return undefined;
|
|
1408
|
+
queueMicrotask(() => {
|
|
1409
|
+
try {
|
|
1410
|
+
fn(message, sendResponse);
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
sendResponse({ success: false, error: String(err), ok: false });
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
return true;
|
|
1416
|
+
});
|