@wenathlan/extension 1.1.33 → 1.1.35

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.
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  (() => {
3
3
  // policy.ts
4
- var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor"]);
4
+ var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen"]);
5
5
  var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
6
- var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath"]);
6
+ var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe"]);
7
7
  var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
8
8
  function parseoptions(step) {
9
9
  if (step.options === void 0) return {};
@@ -440,6 +440,19 @@
440
440
  }
441
441
  return { ok: true, document: current };
442
442
  }
443
+ function describescopes(root) {
444
+ const elements = [...root.querySelectorAll("*")];
445
+ const shadows = [];
446
+ for (const element of elements) {
447
+ const shadow = element.shadowRoot;
448
+ if (shadow) {
449
+ const nested = describescopes(shadow);
450
+ nested.host = summarize(element);
451
+ shadows.push(nested);
452
+ }
453
+ }
454
+ return { candidates: elements.map(summarize), shadows };
455
+ }
443
456
  function queryshadowchain(root, selectors) {
444
457
  let scope = root;
445
458
  for (let position = 0; position < selectors.length; position += 1) {
@@ -1122,6 +1135,323 @@
1122
1135
  return { ok: false, summary: "Unsupported interaction action." };
1123
1136
  }
1124
1137
 
1138
+ // extension/pagenav.ts
1139
+ function loadphase(readystate) {
1140
+ if (readystate === "interactive") return "interactive";
1141
+ if (readystate === "complete") return "complete";
1142
+ return "loading";
1143
+ }
1144
+ function parseurlpattern(step, key = "urlpattern") {
1145
+ let options = {};
1146
+ try {
1147
+ options = parseoptions(step);
1148
+ } catch {
1149
+ options = {};
1150
+ }
1151
+ const value = options[key];
1152
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1153
+ const pattern = value;
1154
+ if (typeof pattern.url !== "string" || !pattern.url) return null;
1155
+ const mode = pattern.mode === "exact" || pattern.mode === "host" || pattern.mode === "pattern" ? pattern.mode : "prefix";
1156
+ const query = {};
1157
+ if (pattern.query && typeof pattern.query === "object" && !Array.isArray(pattern.query)) {
1158
+ for (const [name, item] of Object.entries(pattern.query)) if (typeof item === "string") query[name] = item;
1159
+ }
1160
+ return {
1161
+ mode,
1162
+ url: pattern.url,
1163
+ ...Object.keys(query).length > 0 ? { query } : {},
1164
+ ...typeof pattern.fragment === "string" && pattern.fragment ? { fragment: pattern.fragment } : {}
1165
+ };
1166
+ }
1167
+ function segmentmatches(pattern, actual) {
1168
+ if (pattern === "*" || pattern === "**") return true;
1169
+ if (!pattern.includes("*")) return pattern === actual;
1170
+ const parts = pattern.split("*");
1171
+ let index = 0;
1172
+ for (let position = 0; position < parts.length; position += 1) {
1173
+ const part = parts[position];
1174
+ if (part === "") continue;
1175
+ const found = actual.indexOf(part, index);
1176
+ if (found < 0) return false;
1177
+ if (position === 0 && found !== 0) return false;
1178
+ index = found + part.length;
1179
+ }
1180
+ const last = parts[parts.length - 1];
1181
+ return last === "" || actual.endsWith(last);
1182
+ }
1183
+ function urlmatches(url, pattern) {
1184
+ let parsed;
1185
+ try {
1186
+ parsed = new URL(url);
1187
+ } catch {
1188
+ return false;
1189
+ }
1190
+ let expected;
1191
+ try {
1192
+ expected = new URL(pattern.url);
1193
+ } catch {
1194
+ return false;
1195
+ }
1196
+ if (pattern.mode === "exact" && parsed.toString() !== expected.toString()) return false;
1197
+ if (pattern.mode === "prefix" && !parsed.toString().startsWith(pattern.url)) return false;
1198
+ if (pattern.mode === "host" && parsed.origin !== expected.origin) return false;
1199
+ if (pattern.mode === "pattern") {
1200
+ if (parsed.origin !== expected.origin) return false;
1201
+ const expectedsegments = expected.pathname.split("/").filter((segment) => segment !== "");
1202
+ const actualsegments = parsed.pathname.split("/").filter((segment) => segment !== "");
1203
+ if (expectedsegments.includes("**")) {
1204
+ const cut = expectedsegments.indexOf("**");
1205
+ const head = expectedsegments.slice(0, cut);
1206
+ const tail = expectedsegments.slice(cut + 1);
1207
+ if (actualsegments.length < head.length + tail.length) return false;
1208
+ if (!head.every((segment, position) => segmentmatches(segment, actualsegments[position] ?? ""))) return false;
1209
+ if (!tail.every((segment, position) => segmentmatches(segment, actualsegments[actualsegments.length - tail.length + position] ?? ""))) return false;
1210
+ } else if (expectedsegments.length !== actualsegments.length || !expectedsegments.every((segment, position) => segmentmatches(segment, actualsegments[position] ?? ""))) return false;
1211
+ }
1212
+ const values = parsed.searchParams;
1213
+ for (const [name, value] of Object.entries(pattern.query ?? {})) {
1214
+ if (!values.has(name)) return false;
1215
+ if (value !== "*" && values.get(name) !== value) return false;
1216
+ }
1217
+ if (pattern.fragment !== void 0 && parsed.hash.slice(1) !== pattern.fragment) return false;
1218
+ return true;
1219
+ }
1220
+ function readquery(url) {
1221
+ const values = {};
1222
+ try {
1223
+ for (const [name, value] of new URL(url).searchParams.entries()) values[name] = value;
1224
+ } catch {
1225
+ }
1226
+ return values;
1227
+ }
1228
+ function rewritequeryurl(url, set, remove) {
1229
+ const parsed = new URL(url);
1230
+ const before = readquery(url);
1231
+ for (const name of remove) parsed.searchParams.delete(name);
1232
+ for (const [name, value] of Object.entries(set)) parsed.searchParams.set(name, value);
1233
+ parsed.hash = "";
1234
+ return { url: parsed.toString(), before, after: readquery(parsed.toString()), set: Object.keys(set), removed: [...remove] };
1235
+ }
1236
+ function fragmenturl(url, fragment) {
1237
+ const parsed = new URL(url);
1238
+ parsed.hash = fragment.replace(/^#/, "");
1239
+ return parsed.toString();
1240
+ }
1241
+ function matchlinktext(links, text) {
1242
+ const wanted = text.trim().toLowerCase();
1243
+ return links.filter((link) => link.text.trim().toLowerCase() === wanted);
1244
+ }
1245
+ function matchlinkfragment(links, fragment) {
1246
+ const wanted = fragment.trim().replace(/^#/, "");
1247
+ return links.filter((link) => {
1248
+ try {
1249
+ return new URL(link.href, "https://example.invalid").hash.replace(/^#/, "") === wanted;
1250
+ } catch {
1251
+ return false;
1252
+ }
1253
+ });
1254
+ }
1255
+ function spauroutechanged(previousurl, currenturl) {
1256
+ if (previousurl === currenturl) return false;
1257
+ try {
1258
+ return new URL(previousurl).origin === new URL(currenturl).origin;
1259
+ } catch {
1260
+ return false;
1261
+ }
1262
+ }
1263
+ function wait2(ms) {
1264
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
1265
+ }
1266
+ function stepoptions(step) {
1267
+ try {
1268
+ return parseoptions(step);
1269
+ } catch {
1270
+ return {};
1271
+ }
1272
+ }
1273
+ function collectlinks(root) {
1274
+ return [...root.querySelectorAll("a[href]")].map((element) => ({
1275
+ text: element.textContent?.trim() ?? "",
1276
+ href: element instanceof HTMLAnchorElement ? element.href : element.getAttribute("href") ?? "",
1277
+ selector: element.getAttribute("href") ?? ""
1278
+ }));
1279
+ }
1280
+ function resolvelink(step, root) {
1281
+ const options = stepoptions(step);
1282
+ const allowedorigins = Array.isArray(options.allowedorigins) ? options.allowedorigins.filter((item) => typeof item === "string") : [];
1283
+ const links = collectlinks(root);
1284
+ const matches = options.fragment === true ? matchlinkfragment(links, step.value ?? "") : matchlinktext(links, step.value ?? "");
1285
+ if (matches.length === 0) return { ok: false, summary: `No link matches the reviewed reference "${step.value ?? ""}".` };
1286
+ if (matches.length > 1) return { ok: false, summary: `The reviewed link reference matched ${matches.length} links; review a unique one.` };
1287
+ const element = [...root.querySelectorAll("a[href]")].find((candidate) => (candidate instanceof HTMLAnchorElement ? candidate.href : candidate.getAttribute("href") ?? "") === matches[0]?.href);
1288
+ if (!(element instanceof HTMLAnchorElement)) return { ok: false, summary: "The reviewed link is no longer available." };
1289
+ if (allowedorigins.length > 0) {
1290
+ let origin = "";
1291
+ try {
1292
+ origin = new URL(element.href).origin;
1293
+ } catch {
1294
+ origin = "";
1295
+ }
1296
+ if (!allowedorigins.includes(origin)) return { ok: false, summary: `The reviewed link leaves the session origin grants for ${origin}.` };
1297
+ }
1298
+ return { ok: true, element };
1299
+ }
1300
+ async function runwaitload(step) {
1301
+ const options = stepoptions(step);
1302
+ const timeout = typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0 ? options.timeout : 0;
1303
+ const started = Date.now();
1304
+ for (; ; ) {
1305
+ const phase = loadphase(document.readyState);
1306
+ if (phase === "complete") return { ok: true, summary: `The page load event fired and the document is complete after ${Date.now() - started} milliseconds.`, details: { phase, readystate: document.readyState, waited: Date.now() - started } };
1307
+ if (timeout > 0 && Date.now() - started >= timeout) return { ok: false, summary: `The page did not reach the complete load phase within the reviewed timeout of ${timeout} milliseconds.`, details: { phase, readystate: document.readyState, waited: Date.now() - started } };
1308
+ await wait2(50);
1309
+ }
1310
+ }
1311
+ async function runwaiturl(step) {
1312
+ const options = stepoptions(step);
1313
+ const pattern = parseurlpattern(step);
1314
+ if (!pattern) return { ok: false, summary: "A reviewed urlpattern is required." };
1315
+ const timeout = typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0 ? options.timeout : 0;
1316
+ const poll2 = typeof options.poll === "number" && Number.isFinite(options.poll) && options.poll > 0 ? options.poll : 100;
1317
+ const started = Date.now();
1318
+ for (; ; ) {
1319
+ if (urlmatches(location.href, pattern)) return { ok: true, summary: `The url matched the reviewed ${pattern.mode} pattern after ${Date.now() - started} milliseconds.`, details: { url: location.href, mode: pattern.mode, waited: Date.now() - started } };
1320
+ if (timeout > 0 && Date.now() - started >= timeout) return { ok: false, summary: `The url did not match the reviewed ${pattern.mode} pattern within the reviewed timeout of ${timeout} milliseconds.`, details: { url: location.href, mode: pattern.mode, waited: Date.now() - started } };
1321
+ await wait2(poll2);
1322
+ }
1323
+ }
1324
+ async function runfollowlink(step, root) {
1325
+ const resolution = resolvelink(step, root);
1326
+ if (!resolution.ok) return { ok: false, summary: resolution.summary };
1327
+ const href = resolution.element.href;
1328
+ resolution.element.click();
1329
+ return { ok: true, summary: `Followed the reviewed link to ${href}.`, details: { href } };
1330
+ }
1331
+ async function runspanav(step, root) {
1332
+ const options = stepoptions(step);
1333
+ const resolution = resolvelink(step, root);
1334
+ if (!resolution.ok) return { ok: false, summary: resolution.summary };
1335
+ const timeout = typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0 ? options.timeout : 0;
1336
+ const pattern = parseurlpattern(step, "routepattern");
1337
+ const before = location.href;
1338
+ resolution.element.click();
1339
+ const started = Date.now();
1340
+ for (; ; ) {
1341
+ const changed = spauroutechanged(before, location.href);
1342
+ const matched = pattern ? urlmatches(location.href, pattern) : changed;
1343
+ if (matched) return { ok: true, summary: `The single page app route changed to ${location.href} without a reload.`, details: { from: before, to: location.href, waited: Date.now() - started } };
1344
+ if (timeout > 0 && Date.now() - started >= timeout) return { ok: false, summary: `The single page app route did not change within the reviewed timeout of ${timeout} milliseconds.`, details: { from: before, to: location.href, waited: Date.now() - started } };
1345
+ await wait2(50);
1346
+ }
1347
+ }
1348
+ async function runspawait(step) {
1349
+ const options = stepoptions(step);
1350
+ const timeout = typeof options.timeout === "number" && Number.isFinite(options.timeout) && options.timeout > 0 ? options.timeout : 0;
1351
+ const poll2 = typeof options.poll === "number" && Number.isFinite(options.poll) && options.poll > 0 ? options.poll : 100;
1352
+ const pattern = parseurlpattern(step);
1353
+ const before = location.href;
1354
+ const started = Date.now();
1355
+ let detected = false;
1356
+ const onroute = () => {
1357
+ if (spauroutechanged(before, location.href)) detected = true;
1358
+ };
1359
+ window.addEventListener("popstate", onroute);
1360
+ window.addEventListener("hashchange", onroute);
1361
+ try {
1362
+ for (; ; ) {
1363
+ if (pattern ? urlmatches(location.href, pattern) : detected || spauroutechanged(before, location.href)) {
1364
+ return { ok: true, summary: `The single page app url changed to ${location.href} without a reload.`, details: { from: before, to: location.href, waited: Date.now() - started } };
1365
+ }
1366
+ if (timeout > 0 && Date.now() - started >= timeout) return { ok: false, summary: `The single page app url did not change within the reviewed timeout of ${timeout} milliseconds.`, details: { from: before, to: location.href, waited: Date.now() - started } };
1367
+ await wait2(poll2);
1368
+ }
1369
+ } finally {
1370
+ window.removeEventListener("popstate", onroute);
1371
+ window.removeEventListener("hashchange", onroute);
1372
+ }
1373
+ }
1374
+ function runrewritequery(step) {
1375
+ const options = stepoptions(step);
1376
+ const set = {};
1377
+ if (options.set && typeof options.set === "object" && !Array.isArray(options.set)) {
1378
+ for (const [name, value] of Object.entries(options.set)) if (typeof value === "string") set[name] = value;
1379
+ }
1380
+ const remove = Array.isArray(options.remove) ? options.remove.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
1381
+ const outcome = rewritequeryurl(location.href, set, remove);
1382
+ history.pushState(history.state, document.title, outcome.url);
1383
+ return { ok: true, summary: `Rewrote ${outcome.set.length + outcome.removed.length} query parameter${outcome.set.length + outcome.removed.length === 1 ? "" : "s"}; the url is now ${outcome.url}.`, details: { url: outcome.url, before: outcome.before, after: outcome.after, set: outcome.set, removed: outcome.removed } };
1384
+ }
1385
+ async function runsetfragment(step) {
1386
+ const fragment = (step.value ?? "").replace(/^#/, "");
1387
+ if (!fragment) return { ok: false, summary: "A reviewed fragment is required." };
1388
+ const url = fragmenturl(location.href, fragment);
1389
+ history.pushState(history.state, document.title, url);
1390
+ const anchor = document.getElementById(fragment);
1391
+ anchor?.scrollIntoView({ behavior: "smooth", block: "start" });
1392
+ return { ok: true, summary: `Set the url fragment to ${fragment} and scrolled to its anchor.`, details: { url, fragment, anchored: Boolean(anchor) } };
1393
+ }
1394
+ function runstopnav() {
1395
+ window.stop();
1396
+ return { ok: true, summary: "Stopped the pending navigation of the page." };
1397
+ }
1398
+ function runprefetch(step) {
1399
+ const options = stepoptions(step);
1400
+ const urls = Array.isArray(options.urls) ? options.urls.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
1401
+ if (urls.length === 0) return { ok: false, summary: "A reviewed list of prefetch urls is required." };
1402
+ for (const url of urls) {
1403
+ const hint = document.createElement("link");
1404
+ hint.rel = "prefetch";
1405
+ hint.href = url;
1406
+ document.head.append(hint);
1407
+ }
1408
+ return { ok: true, summary: `Queued ${urls.length} prefetch hint${urls.length === 1 ? "" : "s"}.`, details: { urls } };
1409
+ }
1410
+ function runpreconnect(step) {
1411
+ const options = stepoptions(step);
1412
+ const origins = Array.isArray(options.origins) ? options.origins.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
1413
+ if (origins.length === 0) return { ok: false, summary: "A reviewed list of preconnect origins is required." };
1414
+ for (const origin of origins) {
1415
+ const hint = document.createElement("link");
1416
+ hint.rel = "preconnect";
1417
+ hint.href = origin;
1418
+ document.head.append(hint);
1419
+ }
1420
+ return { ok: true, summary: `Opened ${origins.length} preconnect hint${origins.length === 1 ? "" : "s"}.`, details: { origins } };
1421
+ }
1422
+ function runprintpdf() {
1423
+ window.print();
1424
+ return { ok: true, summary: "Sent the page to the browser print pipeline." };
1425
+ }
1426
+ function runpagenav(step, root = document) {
1427
+ switch (step.kind) {
1428
+ case "waitload":
1429
+ return runwaitload(step);
1430
+ case "waiturl":
1431
+ return runwaiturl(step);
1432
+ case "followlink":
1433
+ return runfollowlink(step, root);
1434
+ case "spanav":
1435
+ return runspanav(step, root);
1436
+ case "spawait":
1437
+ return runspawait(step);
1438
+ case "rewritequery":
1439
+ return runrewritequery(step);
1440
+ case "setfragment":
1441
+ return runsetfragment(step);
1442
+ case "stopnav":
1443
+ return runstopnav();
1444
+ case "prefetch":
1445
+ return runprefetch(step);
1446
+ case "preconnect":
1447
+ return runpreconnect(step);
1448
+ case "printpdf":
1449
+ return runprintpdf();
1450
+ default:
1451
+ return { ok: false, summary: "Unsupported navigation action." };
1452
+ }
1453
+ }
1454
+
1125
1455
  // extension/pagedialogs.ts
1126
1456
  function harvestdialoglog(root) {
1127
1457
  const raw = root.documentElement.dataset.devthinkdialoglog;
@@ -1136,8 +1466,900 @@
1136
1466
  }
1137
1467
  }
1138
1468
 
1469
+ // extension/pageobserve.ts
1470
+ var maxframedepth = 4;
1471
+ function elementstates(element) {
1472
+ const states = [];
1473
+ if (element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true") states.push("disabled");
1474
+ if (element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio") && element.checked) states.push("checked");
1475
+ const expanded = element.getAttribute("aria-expanded");
1476
+ if (expanded !== null) states.push(`expanded ${expanded}`);
1477
+ if (element.getAttribute("aria-selected") === "true") states.push("selected");
1478
+ if (element.hasAttribute("required") || element.getAttribute("aria-required") === "true") states.push("required");
1479
+ if (element.hasAttribute("readonly") || element.getAttribute("aria-readonly") === "true") states.push("readonly");
1480
+ if (element.getAttribute("aria-hidden") === "true") states.push("hidden");
1481
+ return states;
1482
+ }
1483
+ function elementvalue(element) {
1484
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) return element.value;
1485
+ return "";
1486
+ }
1487
+ function elementhidden(element) {
1488
+ if (element instanceof HTMLInputElement && element.type === "hidden") return true;
1489
+ if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true") return true;
1490
+ try {
1491
+ const style = element.ownerDocument?.defaultView?.getComputedStyle(element);
1492
+ if (style && (style.display === "none" || style.visibility === "hidden")) return true;
1493
+ } catch {
1494
+ }
1495
+ return false;
1496
+ }
1497
+ function framenode(frame, depth) {
1498
+ let content = null;
1499
+ try {
1500
+ content = frame.contentDocument;
1501
+ } catch {
1502
+ content = null;
1503
+ }
1504
+ let sameorigin = false;
1505
+ try {
1506
+ sameorigin = content !== null && frame.contentWindow?.location.origin === location.origin;
1507
+ } catch {
1508
+ sameorigin = false;
1509
+ }
1510
+ const node = wrap2(frame, depth);
1511
+ if (sameorigin && content && depth < maxframedepth) node.children.push(...wrapchildren(content, depth + 1));
1512
+ return node;
1513
+ }
1514
+ function wrap2(element, depth) {
1515
+ const shadow = element.shadowRoot;
1516
+ const node = {
1517
+ tag: element.tagName.toLowerCase(),
1518
+ selector: elementselector(element),
1519
+ id: element.id,
1520
+ classes: [...element.classList],
1521
+ role: element.getAttribute("role")?.toLowerCase() || implicitrole(element),
1522
+ name: elementlabel(element),
1523
+ text: owntext2(element),
1524
+ value: elementvalue(element),
1525
+ states: elementstates(element),
1526
+ hidden: elementhidden(element),
1527
+ children: [],
1528
+ element
1529
+ };
1530
+ if (shadow) node.children.push(...wrapchildren(shadow, depth));
1531
+ if (element instanceof HTMLIFrameElement) return framenode(element, depth);
1532
+ node.children.push(...wrapchildren(element, depth));
1533
+ return node;
1534
+ }
1535
+ function wrapchildren(scope, depth) {
1536
+ return [...scope.querySelectorAll(":scope > *")].map((child) => wrap2(child, depth));
1537
+ }
1538
+ function buildpagetree(scope) {
1539
+ const root = {
1540
+ tag: "#document",
1541
+ selector: "",
1542
+ id: "",
1543
+ classes: [],
1544
+ role: "document",
1545
+ name: "",
1546
+ text: "",
1547
+ value: "",
1548
+ states: [],
1549
+ hidden: false,
1550
+ children: []
1551
+ };
1552
+ if (scope instanceof Document) {
1553
+ root.children = scope.documentElement ? [wrap2(scope.documentElement, 0)] : [];
1554
+ } else {
1555
+ root.children = [...wrapchildren(scope, 0)];
1556
+ }
1557
+ return root;
1558
+ }
1559
+ function countnodes(node) {
1560
+ return 1 + node.children.reduce((total, child) => total + countnodes(child), 0);
1561
+ }
1562
+ function builda11ytree(node) {
1563
+ const children = node.children.filter((child) => !child.hidden).map(builda11ytree);
1564
+ return {
1565
+ role: node.role || "generic",
1566
+ name: node.name,
1567
+ states: node.states,
1568
+ ...node.value ? { value: node.value } : {},
1569
+ childcount: children.length,
1570
+ children
1571
+ };
1572
+ }
1573
+ function visibleentries(node) {
1574
+ if (node.hidden) return [];
1575
+ const entries = node.text ? [{ selector: node.selector || node.tag, text: node.text }] : [];
1576
+ for (const child of node.children) entries.push(...visibleentries(child));
1577
+ return entries;
1578
+ }
1579
+ function visibletext(node) {
1580
+ return visibleentries(node).map((entry) => entry.text).join(" ");
1581
+ }
1582
+ function nodetextlength(node) {
1583
+ return node.text.length + node.children.reduce((total, child) => total + nodetextlength(child), 0);
1584
+ }
1585
+ function nodelinktext(node) {
1586
+ const own = node.tag === "a" ? node.text.length : 0;
1587
+ return own + node.children.reduce((total, child) => total + nodelinktext(child), 0);
1588
+ }
1589
+ function wordsin(text) {
1590
+ return text.split(/\s+/).filter(Boolean).length;
1591
+ }
1592
+ function findbyline(node) {
1593
+ const markers = ["byline", "author"];
1594
+ const direct = node.classes.some((item) => markers.some((marker) => item.toLowerCase().includes(marker))) || markers.some((marker) => node.id.toLowerCase().includes(marker));
1595
+ if (direct && node.text) return node.text;
1596
+ for (const child of node.children) {
1597
+ const found = findbyline(child);
1598
+ if (found) return found;
1599
+ }
1600
+ return "";
1601
+ }
1602
+ function findheading(node, tags) {
1603
+ if (tags.includes(node.tag) && node.text) return node.text;
1604
+ for (const child of node.children) {
1605
+ const found = findheading(child, tags);
1606
+ if (found) return found;
1607
+ }
1608
+ return "";
1609
+ }
1610
+ function buildreader(root, title) {
1611
+ let best;
1612
+ let bestscore = 0;
1613
+ const walk = (node) => {
1614
+ if (node.tag !== "#document") {
1615
+ const length = nodetextlength(node);
1616
+ const links = nodelinktext(node);
1617
+ const score = length * (1 - (length > 0 ? links / length : 0));
1618
+ if (score > bestscore) {
1619
+ bestscore = score;
1620
+ best = node;
1621
+ }
1622
+ }
1623
+ for (const child of node.children) walk(child);
1624
+ };
1625
+ walk(root);
1626
+ const article = best ?? root;
1627
+ const blocks = article.children.filter((child) => !child.hidden && child.text).map((child) => ({ kind: child.tag, text: child.text, words: wordsin(child.text) }));
1628
+ const ownblock = article.text ? [{ kind: article.tag, text: article.text, words: wordsin(article.text) }] : [];
1629
+ const allblocks = [...ownblock, ...blocks];
1630
+ return {
1631
+ title: findheading(article, ["h1"]) || findheading(root, ["h1"]) || title,
1632
+ byline: findbyline(article) || findbyline(root),
1633
+ blocks: allblocks,
1634
+ words: allblocks.reduce((total, block) => total + block.words, 0),
1635
+ characters: allblocks.reduce((total, block) => total + block.text.length, 0)
1636
+ };
1637
+ }
1638
+ function pageoutline(root, title) {
1639
+ const headings = [];
1640
+ const walk = (node) => {
1641
+ const level = /^h([1-6])$/.exec(node.tag);
1642
+ if (level && node.text) headings.push({ level: Number.parseInt(level[1], 10), text: node.text });
1643
+ for (const child of node.children) walk(child);
1644
+ };
1645
+ walk(root);
1646
+ return { title: findheading(root, ["h1"]) || title, headings };
1647
+ }
1648
+ function captureselection(root) {
1649
+ const selection = root.getSelection?.() ?? null;
1650
+ const text = selection ? clean(selection.toString()) : "";
1651
+ return { text, length: text.length };
1652
+ }
1653
+ function opengraphfields(meta, jsonld) {
1654
+ const graph = {};
1655
+ for (const entry of meta) {
1656
+ if (entry.property.startsWith("og:") && entry.content) graph[entry.property] = entry.content;
1657
+ }
1658
+ const structured = [];
1659
+ let refused = 0;
1660
+ for (const raw of jsonld) {
1661
+ try {
1662
+ structured.push(JSON.parse(raw));
1663
+ } catch {
1664
+ refused += 1;
1665
+ }
1666
+ }
1667
+ return { graph, structured, refused };
1668
+ }
1669
+ var stopwords = {
1670
+ en: ["the", "is", "at", "which", "on", "and", "of", "to", "in", "that", "it", "with"],
1671
+ pt: ["de", "que", "n\xE3o", "uma", "para", "com", "por", "mais", "como", "p\xE1gina", "este", "voc\xEA"],
1672
+ es: ["que", "el", "las", "los", "por", "una", "para", "con", "como", "p\xE1gina", "m\xE1s", "este"],
1673
+ fr: ["le", "les", "des", "que", "pour", "dans", "est", "sur", "avec", "page", "plus", "cette"],
1674
+ de: ["der", "die", "und", "das", "ist", "von", "mit", "f\xFCr", "auf", "den", "nicht", "seite"],
1675
+ it: ["che", "il", "la", "per", "una", "del", "sono", "non", "con", "pagina", "pi\xF9", "questo"],
1676
+ nl: ["het", "een", "en", "van", "is", "dat", "op", "te", "voor", "met", "niet", "pagina"]
1677
+ };
1678
+ function detecttextlanguage(text) {
1679
+ const words = text.toLowerCase().split(/[^a-zà-ÿ]+/).filter(Boolean);
1680
+ if (words.length === 0) return "";
1681
+ let best = "";
1682
+ let bestscore = 0;
1683
+ for (const [language, dictionary] of Object.entries(stopwords)) {
1684
+ const score = words.filter((word) => dictionary.includes(word)).length;
1685
+ if (score > bestscore) {
1686
+ bestscore = score;
1687
+ best = language;
1688
+ }
1689
+ }
1690
+ return best;
1691
+ }
1692
+ function taglanguage(text) {
1693
+ return { text, language: detecttextlanguage(text) };
1694
+ }
1695
+ function documentlanguage(signals) {
1696
+ if (signals.lang.trim()) return { language: signals.lang.trim(), source: "document" };
1697
+ if (signals.meta.trim()) return { language: signals.meta.trim(), source: "meta" };
1698
+ return { language: detecttextlanguage(signals.text), source: "content" };
1699
+ }
1700
+ function shadowpaths(scope) {
1701
+ const paths = [];
1702
+ const walk = (tree, prefix) => {
1703
+ for (const shadow of tree.shadows) {
1704
+ if (!shadow.host) continue;
1705
+ const path = prefix ? `${prefix} > ${shadow.host.selector}` : shadow.host.selector;
1706
+ paths.push(path);
1707
+ walk(shadow, path);
1708
+ }
1709
+ };
1710
+ walk(scope, "");
1711
+ return paths;
1712
+ }
1713
+ function framelist(root) {
1714
+ return [...root.querySelectorAll("iframe")].map((frame, index) => {
1715
+ let origin = "";
1716
+ try {
1717
+ origin = frame.contentWindow?.location.origin ?? "";
1718
+ } catch {
1719
+ origin = "";
1720
+ }
1721
+ const rect = frame.getBoundingClientRect();
1722
+ return { index, origin, sameorigin: origin !== "" && origin === location.origin, width: Math.round(rect.width), height: Math.round(rect.height) };
1723
+ });
1724
+ }
1725
+ function stepoptions2(step) {
1726
+ try {
1727
+ return parseoptions(step);
1728
+ } catch {
1729
+ return {};
1730
+ }
1731
+ }
1732
+ function runpageobservation(step, target, root = document) {
1733
+ switch (step.kind) {
1734
+ case "a11ytree": {
1735
+ const tree = builda11ytree(buildpagetree(root));
1736
+ const count = countnodes(tree);
1737
+ return { ok: true, summary: `Captured the accessibility tree with ${count} node${count === 1 ? "" : "s"}.`, details: { tree, nodecount: count } };
1738
+ }
1739
+ case "readvisible": {
1740
+ const scope = target ?? root;
1741
+ const tree = buildpagetree(scope);
1742
+ const entries = visibleentries(tree);
1743
+ return { ok: true, summary: `Read the rendered text of ${entries.length} visible element${entries.length === 1 ? "" : "s"}.`, details: { entries, text: visibletext(tree) } };
1744
+ }
1745
+ case "readertree": {
1746
+ const article = buildreader(buildpagetree(root), root.title);
1747
+ return { ok: true, summary: `Extracted the reader view with ${article.blocks.length} block${article.blocks.length === 1 ? "" : "s"} and ${article.words} words.`, details: { article } };
1748
+ }
1749
+ case "readoutline": {
1750
+ const outline = pageoutline(buildpagetree(root), root.title);
1751
+ return { ok: true, summary: `Read the outline with ${outline.headings.length} heading${outline.headings.length === 1 ? "" : "s"}.`, details: { title: outline.title, headings: outline.headings } };
1752
+ }
1753
+ case "readselection": {
1754
+ const selection = captureselection(root);
1755
+ return { ok: true, summary: selection.text ? `Read ${selection.length} characters of the current selection.` : "No text is currently selected.", details: { text: selection.text, length: selection.length } };
1756
+ }
1757
+ case "readopengraph": {
1758
+ const meta = [...root.querySelectorAll("meta")].map((element) => ({ property: element.getAttribute("property") ?? "", name: element.getAttribute("name") ?? "", content: element.getAttribute("content") ?? "" }));
1759
+ const jsonld = [...root.querySelectorAll('script[type="application/ld+json"]')].map((element) => element.textContent ?? "");
1760
+ const fields = opengraphfields(meta, jsonld);
1761
+ return { ok: true, summary: `Read ${Object.keys(fields.graph).length} open graph entr${Object.keys(fields.graph).length === 1 ? "y" : "ies"} and ${fields.structured.length} structured payload${fields.structured.length === 1 ? "" : "s"}${fields.refused > 0 ? `; ${fields.refused} malformed payload${fields.refused === 1 ? " was" : "s were"} refused` : ""}.`, details: { graph: fields.graph, structured: fields.structured, refused: fields.refused } };
1762
+ }
1763
+ case "readlang": {
1764
+ const metatag = root.querySelector('meta[http-equiv="content-language"]')?.getAttribute("content") ?? "";
1765
+ const outcome = documentlanguage({ lang: root.documentElement?.getAttribute("lang") ?? "", meta: metatag, text: root.body?.innerText ?? "" });
1766
+ return { ok: true, summary: `Detected page language ${outcome.language || "unknown"} from the ${outcome.source} signal.`, details: { language: outcome.language, source: outcome.source } };
1767
+ }
1768
+ case "detectlanguage": {
1769
+ const options = stepoptions2(step);
1770
+ const text = typeof options.text === "string" && options.text ? options.text : target?.textContent ?? root.body?.innerText ?? "";
1771
+ const routed = taglanguage(clean(text));
1772
+ return { ok: routed.language !== "", summary: routed.language ? `Detected language ${routed.language} for the extracted text.` : "The extracted text language is undetermined.", details: { language: routed.language, routed } };
1773
+ }
1774
+ case "listshadow": {
1775
+ const paths = shadowpaths(describescopes(root));
1776
+ return { ok: true, summary: `Listed ${paths.length} open shadow root${paths.length === 1 ? "" : "s"}.`, details: { shadows: paths } };
1777
+ }
1778
+ case "listframes": {
1779
+ const frames = framelist(root);
1780
+ return { ok: true, summary: `Listed ${frames.length} iframe${frames.length === 1 ? "" : "s"}.`, details: { frames } };
1781
+ }
1782
+ default:
1783
+ return { ok: false, summary: "Unsupported page observation." };
1784
+ }
1785
+ }
1786
+
1787
+ // extension/pagedetect.ts
1788
+ function detectlistpatterns(samples) {
1789
+ const patterns = [];
1790
+ for (const sample of samples) {
1791
+ const groups = /* @__PURE__ */ new Map();
1792
+ for (const child of sample.children) {
1793
+ const key = `${child.tag}|${child.classes}`;
1794
+ const group = groups.get(key) ?? [];
1795
+ group.push(child);
1796
+ groups.set(key, group);
1797
+ }
1798
+ for (const [key, group] of groups) {
1799
+ if (group.length < 2) continue;
1800
+ if (!group.some((item) => item.text)) continue;
1801
+ const [tag, classes] = key.split("|");
1802
+ const classpart = (classes ?? "").split(" ").filter(Boolean).map((name) => `.${name}`).join("");
1803
+ patterns.push({ container: sample.container, itemselector: `${tag}${classpart}`, repeat: group.length, samples: group.map((item) => item.text).filter(Boolean) });
1804
+ }
1805
+ }
1806
+ return patterns;
1807
+ }
1808
+ function normalizetable(rows, caption) {
1809
+ const firstheader = rows.find((row) => row.header);
1810
+ const headers = firstheader?.cells ?? [];
1811
+ const body = firstheader ? rows.filter((row) => row !== firstheader) : rows;
1812
+ const width = rows.reduce((largest, row) => Math.max(largest, row.cells.length), 0);
1813
+ const columns = [];
1814
+ for (let index = 0; index < width; index += 1) {
1815
+ const label = headers[index] ?? `column ${index + 1}`;
1816
+ const cells = body.filter((row) => Boolean((row.cells[index] ?? "").trim())).length;
1817
+ columns.push({ label, cells });
1818
+ }
1819
+ return { headers, columns, rows: body.length, caption };
1820
+ }
1821
+ function paginationestimate(entries) {
1822
+ const pages = [];
1823
+ let current = 0;
1824
+ for (const entry of entries) {
1825
+ const parsed = /^\d+$/.exec(entry.text.trim());
1826
+ if (parsed) {
1827
+ const page = Number.parseInt(parsed[0], 10);
1828
+ pages.push(page);
1829
+ if (entry.current) current = page;
1830
+ }
1831
+ }
1832
+ const total = Math.max(0, ...pages, current);
1833
+ return { current, total, links: entries.length, pages };
1834
+ }
1835
+ function infinitescrollranges(ranges) {
1836
+ return ranges.filter((range) => range.scrollheight > range.clientheight && range.triggers.length > 0).map((range) => ({ selector: range.selector, scrollrange: range.scrollheight - range.clientheight, triggers: range.triggers }));
1837
+ }
1838
+ function virtualizedcontainers(containers) {
1839
+ const results = [];
1840
+ for (const container of containers) {
1841
+ const first = container.rows[0];
1842
+ if (!first || container.rows.length < 2 || first.height <= 0) continue;
1843
+ if (!container.rows.every((row) => row.height === first.height)) continue;
1844
+ if (container.scrollheight <= container.rows.length * first.height) continue;
1845
+ results.push({ selector: container.selector, rendered: container.rows.length, estimated: Math.floor(container.scrollheight / first.height) });
1846
+ }
1847
+ return results;
1848
+ }
1849
+ function lazysurvey(images) {
1850
+ const lazy = [];
1851
+ const placeholders = [];
1852
+ for (const image of images) {
1853
+ if (image.loading === "lazy") lazy.push({ selector: image.selector, reason: "loading attribute" });
1854
+ else if (image.datasrc) lazy.push({ selector: image.selector, reason: "deferred source" });
1855
+ if (!image.src) placeholders.push({ selector: image.selector, reason: "empty source" });
1856
+ else if (image.src.startsWith("data:")) placeholders.push({ selector: image.selector, reason: "inline data placeholder" });
1857
+ }
1858
+ return { lazy, placeholders };
1859
+ }
1860
+ function overlaygeometry(elements, viewport) {
1861
+ const area = viewport.width * viewport.height;
1862
+ return elements.filter((element) => (element.position === "sticky" || element.position === "fixed") && element.top <= 0 && element.height > 0).map((element) => {
1863
+ const coverage = area > 0 ? element.height * element.width / area : 0;
1864
+ return { selector: element.selector, position: element.position, coverage: Math.round(coverage * 1e3) / 1e3, hides: coverage >= overlaythreshold };
1865
+ });
1866
+ }
1867
+ function scrolllockstate(signals) {
1868
+ const reasons = [];
1869
+ if (signals.bodyoverflow.includes("hidden") || signals.htmloverflow.includes("hidden")) reasons.push("overflow hidden");
1870
+ if (signals.bodyposition === "fixed") reasons.push("fixed body");
1871
+ if (signals.modal) reasons.push("modal open");
1872
+ return { locked: reasons.length > 0, reasons, scrollable: signals.scrollable };
1873
+ }
1874
+ var consentkeywords = ["cookie", "consent", "gdpr", "lgpd", "privacy", "ccpa"];
1875
+ function bannermatches(candidates, at) {
1876
+ const reports = [];
1877
+ for (const candidate of candidates) {
1878
+ const haystack = `${candidate.id} ${candidate.classes.join(" ")} ${candidate.text}`.toLowerCase();
1879
+ const keyword = consentkeywords.find((word) => haystack.includes(word));
1880
+ if (!keyword) continue;
1881
+ if (!candidate.text && candidate.controls.length === 0) continue;
1882
+ reports.push({ kind: keyword, selector: candidate.selector, text: candidate.text.slice(0, 200), controls: candidate.controls, at });
1883
+ }
1884
+ return reports;
1885
+ }
1886
+ function classifytemplate(signals) {
1887
+ if (signals.password) return "login";
1888
+ if (signals.paragraphs >= 3) return "article";
1889
+ if (signals.tables > 0) return "table";
1890
+ if (signals.forms > 0 && signals.inputs > 0) return "form";
1891
+ if (signals.lists > 0) return "list";
1892
+ return "generic";
1893
+ }
1894
+ function sectionfingerprint(section) {
1895
+ const canonical = [section.tag, String(section.children), String(section.textlength), ...Object.keys(section.attributes).sort().map((key) => `${key}=${section.attributes[key] ?? ""}`)].join("|");
1896
+ let hash = 5381;
1897
+ for (let index = 0; index < canonical.length; index += 1) hash = (hash << 5) + hash + canonical.charCodeAt(index) >>> 0;
1898
+ return `fp${hash.toString(16)}`;
1899
+ }
1900
+ function scrollreport(window2, containers) {
1901
+ const range = Math.max(0, window2.scrollheight - window2.clientheight);
1902
+ return {
1903
+ window: { x: window2.scrollx, y: window2.scrolly, attop: window2.scrolly <= 0, atbottom: window2.scrolly >= range, height: window2.scrollheight },
1904
+ containers: containers.map((container) => {
1905
+ const containerrange = Math.max(0, container.scrollheight - container.clientheight);
1906
+ return { selector: container.selector, scrolltop: container.scrolltop, scrollleft: container.scrollleft, scrollrange: containerrange, atbottom: container.scrolltop >= containerrange };
1907
+ })
1908
+ };
1909
+ }
1910
+ var loadmorepattern = /(load more|show more|see more|ver mais|carregar mais|load older|afficher plus|mehr anzeigen)/i;
1911
+ var paginationtext = /^(next|prev|previous|last|first|next page|previous page|»|«|›|‹|\d+)$/i;
1912
+ var overlaythreshold = 0.25;
1913
+ function signatureof(element) {
1914
+ return `${element.tagName.toLowerCase()}|${[...element.classList].sort().join(" ")}`;
1915
+ }
1916
+ function collectsiblings(root) {
1917
+ const samples = [];
1918
+ for (const element of [...root.querySelectorAll("*")]) {
1919
+ const children = [...element.children];
1920
+ if (children.length < 2) continue;
1921
+ const counts = /* @__PURE__ */ new Map();
1922
+ for (const child of children) {
1923
+ const key = signatureof(child);
1924
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1925
+ }
1926
+ if (![...counts.values()].some((count) => count >= 2)) continue;
1927
+ samples.push({
1928
+ container: elementselector(element),
1929
+ children: children.map((child) => ({ tag: child.tagName.toLowerCase(), classes: [...child.classList].sort().join(" "), text: clean(child.textContent ?? ""), selector: elementselector(child) }))
1930
+ });
1931
+ }
1932
+ return samples;
1933
+ }
1934
+ function collecttables(root) {
1935
+ return [...root.querySelectorAll("table")].map((table) => ({
1936
+ selector: elementselector(table),
1937
+ rows: [...table.querySelectorAll("tr")].map((row) => ({ cells: [...row.querySelectorAll("th, td")].map((cell) => clean(cell.textContent ?? "")), header: Boolean(row.querySelector("th")) })),
1938
+ caption: clean(table.querySelector("caption")?.textContent ?? "")
1939
+ }));
1940
+ }
1941
+ function collectpagination(root) {
1942
+ const entries = [];
1943
+ for (const element of [...root.querySelectorAll("a[href], button, [role=button], [role=link], li, span")]) {
1944
+ const text = clean(element.textContent ?? "");
1945
+ if (!text || !paginationtext.test(text)) continue;
1946
+ if (!element.closest("nav, footer, [class*=pag i], [id*=pag i]")) continue;
1947
+ const current = element.getAttribute("aria-current") === "page" || [...element.classList].some((name) => /current|active|selecionado/i.test(name));
1948
+ entries.push({ text, selector: elementselector(element), current });
1949
+ }
1950
+ return entries;
1951
+ }
1952
+ function collecttriggers(scope) {
1953
+ const triggers = [];
1954
+ for (const element of [...scope.querySelectorAll("button, a[href], [role=button], [class*=loading i], [class*=sentinel i], [class*=spinner i]")]) {
1955
+ const label = clean(element.getAttribute("aria-label") ?? element.textContent ?? "");
1956
+ if (loadmorepattern.test(label)) triggers.push(elementselector(element));
1957
+ }
1958
+ return triggers;
1959
+ }
1960
+ function collectscrollranges(root) {
1961
+ const ranges = [];
1962
+ const scrolling = root.scrollingElement ?? root.documentElement;
1963
+ const viewheight = root.defaultView?.innerHeight ?? 0;
1964
+ if (scrolling && scrolling.scrollHeight > viewheight) ranges.push({ selector: "window", scrollheight: scrolling.scrollHeight, clientheight: viewheight, triggers: collecttriggers(root) });
1965
+ for (const element of [...root.querySelectorAll("*")]) {
1966
+ if (!(element instanceof HTMLElement)) continue;
1967
+ if (element.scrollHeight <= element.clientHeight) continue;
1968
+ ranges.push({ selector: elementselector(element), scrollheight: element.scrollHeight, clientheight: element.clientHeight, triggers: collecttriggers(element) });
1969
+ }
1970
+ return ranges;
1971
+ }
1972
+ function collectvirtual(root) {
1973
+ const containers = [];
1974
+ for (const element of [...root.querySelectorAll("*")]) {
1975
+ const children = [...element.children];
1976
+ const first = children[0];
1977
+ if (!first || children.length < 2) continue;
1978
+ if (!children.every((child) => signatureof(child) === signatureof(first))) continue;
1979
+ const heights = children.map((child) => child.getBoundingClientRect().height);
1980
+ if (!heights.every((height) => height > 0 && height === heights[0])) continue;
1981
+ containers.push({ selector: elementselector(element), scrollheight: element.scrollHeight, rows: children.map((child) => ({ selector: elementselector(child), height: child.getBoundingClientRect().height, classes: [...child.classList].join(" ") })) });
1982
+ }
1983
+ return containers;
1984
+ }
1985
+ function collectimages(root) {
1986
+ return [...root.querySelectorAll("img")].map((image) => ({
1987
+ selector: elementselector(image),
1988
+ src: image.getAttribute("src") ?? "",
1989
+ datasrc: image.getAttribute("data-src") ?? image.getAttribute("data-original") ?? "",
1990
+ loading: image.getAttribute("loading") ?? "",
1991
+ width: image.naturalWidth,
1992
+ height: image.naturalHeight
1993
+ }));
1994
+ }
1995
+ function collectoverlays(root) {
1996
+ const elements = [];
1997
+ for (const element of [...root.querySelectorAll("*")]) {
1998
+ if (!(element instanceof HTMLElement)) continue;
1999
+ const view = element.ownerDocument.defaultView;
2000
+ const position = view ? view.getComputedStyle(element).position : "";
2001
+ if (position !== "sticky" && position !== "fixed") continue;
2002
+ const rect = element.getBoundingClientRect();
2003
+ elements.push({ selector: elementselector(element), position, top: rect.top, height: rect.height, width: rect.width });
2004
+ }
2005
+ return elements;
2006
+ }
2007
+ function collectlocksignals(root) {
2008
+ const view = root.defaultView;
2009
+ const bodystyle = root.body ? view ? view.getComputedStyle(root.body) : void 0 : void 0;
2010
+ const htmlstyle = view ? view.getComputedStyle(root.documentElement) : void 0;
2011
+ return {
2012
+ bodyoverflow: bodystyle?.overflow ?? "",
2013
+ htmloverflow: htmlstyle?.overflow ?? "",
2014
+ bodyposition: bodystyle?.position ?? "",
2015
+ modal: Boolean(root.querySelector("dialog[open], [aria-modal=true]")),
2016
+ scrollable: root.documentElement.scrollHeight > root.documentElement.clientHeight
2017
+ };
2018
+ }
2019
+ var bannerselector = '[id*="cookie" i], [class*="cookie" i], [id*="consent" i], [class*="consent" i], [id*="gdpr" i], [class*="gdpr" i], [id*="privacy" i], [class*="privacy" i], [id*="banner" i], [class*="banner" i], dialog, [role="dialog"], [aria-modal="true"]';
2020
+ function collectbannercandidates(root) {
2021
+ const found = [...root.querySelectorAll(bannerselector)];
2022
+ return found.filter((element) => !found.some((other) => other !== element && other.contains(element))).map((element) => ({
2023
+ selector: elementselector(element),
2024
+ id: element.id,
2025
+ classes: [...element.classList],
2026
+ text: clean(element.textContent ?? "").slice(0, 200),
2027
+ controls: [...element.querySelectorAll("button, a[href], [role=button]")].map((control) => clean(control.getAttribute("aria-label") ?? control.textContent ?? "")).filter(Boolean)
2028
+ }));
2029
+ }
2030
+ function runpagedetection(step, target, root = document) {
2031
+ switch (step.kind) {
2032
+ case "detectlists": {
2033
+ const patterns = detectlistpatterns(collectsiblings(root));
2034
+ return { ok: true, summary: `Detected ${patterns.length} repeated list${patterns.length === 1 ? "" : "s"}.`, details: { lists: patterns } };
2035
+ }
2036
+ case "detecttables": {
2037
+ const tables = collecttables(root).map((entry) => {
2038
+ const shape = normalizetable(entry.rows, entry.caption);
2039
+ return { selector: entry.selector, headers: shape.headers, columns: shape.columns, rows: shape.rows, caption: shape.caption };
2040
+ });
2041
+ return { ok: true, summary: `Detected ${tables.length} data table${tables.length === 1 ? "" : "s"}.`, details: { tables } };
2042
+ }
2043
+ case "countpages": {
2044
+ const estimate = paginationestimate(collectpagination(root));
2045
+ return { ok: true, summary: `Counted ${estimate.links} pagination entr${estimate.links === 1 ? "y" : "ies"} and estimated ${estimate.total} total page${estimate.total === 1 ? "" : "s"}.`, details: { current: estimate.current, total: estimate.total, links: estimate.links, pages: estimate.pages } };
2046
+ }
2047
+ case "detectinfinitescroll": {
2048
+ const containers = infinitescrollranges(collectscrollranges(root));
2049
+ return { ok: true, summary: `Detected ${containers.length} infinite scroll container${containers.length === 1 ? "" : "s"}.`, details: { containers } };
2050
+ }
2051
+ case "detectvirtual": {
2052
+ const containers = virtualizedcontainers(collectvirtual(root));
2053
+ return { ok: true, summary: `Detected ${containers.length} virtualized list${containers.length === 1 ? "" : "s"}.`, details: { containers } };
2054
+ }
2055
+ case "detectlazy": {
2056
+ const survey = lazysurvey(collectimages(root));
2057
+ return { ok: true, summary: `Detected ${survey.lazy.length} lazy image${survey.lazy.length === 1 ? "" : "s"} and ${survey.placeholders.length} placeholder${survey.placeholders.length === 1 ? "" : "s"}.`, details: { lazy: survey.lazy, placeholders: survey.placeholders } };
2058
+ }
2059
+ case "detectsticky": {
2060
+ const overlays = overlaygeometry(collectoverlays(root), { width: root.defaultView?.innerWidth ?? 0, height: root.defaultView?.innerHeight ?? 0 });
2061
+ return { ok: true, summary: `Detected ${overlays.length} sticky or fixed overlay${overlays.length === 1 ? "" : "s"}.`, details: { overlays } };
2062
+ }
2063
+ case "detectscrolllock": {
2064
+ const lock = scrolllockstate(collectlocksignals(root));
2065
+ return { ok: true, summary: lock.locked ? `Scroll is locked: ${lock.reasons.join(", ")}.` : "Scroll is not locked.", details: { locked: lock.locked, reasons: lock.reasons, scrollable: lock.scrollable } };
2066
+ }
2067
+ case "classifypage": {
2068
+ const signals = {
2069
+ paragraphs: root.querySelectorAll("p").length,
2070
+ headings: root.querySelectorAll("h1, h2, h3, h4, h5, h6").length,
2071
+ lists: root.querySelectorAll("ul, ol").length,
2072
+ tables: root.querySelectorAll("table").length,
2073
+ forms: root.querySelectorAll("form").length,
2074
+ inputs: root.querySelectorAll("input, textarea, select").length,
2075
+ password: Boolean(root.querySelector("input[type=password]"))
2076
+ };
2077
+ const template = classifytemplate(signals);
2078
+ const fingerprint = sectionfingerprint({ tag: "body", attributes: {}, children: root.body?.children.length ?? 0, textlength: (root.body?.innerText ?? "").length });
2079
+ return { ok: true, summary: `Classified the page template as ${template}.`, details: { template, fingerprint } };
2080
+ }
2081
+ case "fingerprintsection": {
2082
+ if (!target) return { ok: false, summary: "Fingerprint target is no longer available." };
2083
+ const attributes = {};
2084
+ for (const attribute of [...target.attributes]) attributes[attribute.name] = attribute.value;
2085
+ const fingerprint = sectionfingerprint({ tag: target.tagName.toLowerCase(), attributes, children: target.children.length, textlength: (target.textContent ?? "").length });
2086
+ return { ok: true, summary: `Computed section fingerprint ${fingerprint}.`, details: { fingerprint, section: elementselector(target) } };
2087
+ }
2088
+ case "readscrollpos": {
2089
+ const report = scrollreport(
2090
+ { scrollx: root.defaultView?.scrollX ?? 0, scrolly: root.defaultView?.scrollY ?? 0, scrollheight: root.documentElement.scrollHeight, clientheight: root.defaultView?.innerHeight ?? 0 },
2091
+ [...root.querySelectorAll("*")].filter((element) => element instanceof HTMLElement && element.scrollHeight > element.clientHeight).map((element) => ({ selector: elementselector(element), scrolltop: element.scrollTop, scrollleft: element.scrollLeft, scrollheight: element.scrollHeight, clientheight: element.clientHeight }))
2092
+ );
2093
+ return { ok: true, summary: `Read the scroll position at ${Math.round(report.window.x)},${Math.round(report.window.y)} with ${report.containers.length} scrollable container${report.containers.length === 1 ? "" : "s"}.`, details: { scroll: report } };
2094
+ }
2095
+ default:
2096
+ return { ok: false, summary: "Unsupported page detection." };
2097
+ }
2098
+ }
2099
+
2100
+ // extension/pagewatch.ts
2101
+ var defaultpoll = 250;
2102
+ function parsewatchoptions(step, fallbackid) {
2103
+ let options = {};
2104
+ try {
2105
+ options = parseoptions(step);
2106
+ } catch {
2107
+ options = {};
2108
+ }
2109
+ const scopes = Array.isArray(options.scopes) ? options.scopes.filter((item) => typeof item === "string" && item.trim().length > 0) : void 0;
2110
+ const events3 = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : void 0;
2111
+ const lifetime = typeof options.lifetime === "number" && Number.isFinite(options.lifetime) && options.lifetime > 0 ? options.lifetime : 0;
2112
+ return {
2113
+ watchid: typeof options.watchid === "string" && options.watchid.trim() ? options.watchid : fallbackid,
2114
+ ...scopes ? { scopes } : {},
2115
+ ...events3 ? { events: events3 } : {},
2116
+ lifetime,
2117
+ poll: typeof options.poll === "number" && Number.isFinite(options.poll) && options.poll >= 0 ? options.poll : defaultpoll
2118
+ };
2119
+ }
2120
+ function batchmutations(records, windowms) {
2121
+ const batches = [];
2122
+ let current = [];
2123
+ let opened = -1;
2124
+ for (const record of records) {
2125
+ if (current.length === 0 || windowms > 0 && record.at - opened >= windowms) {
2126
+ if (current.length > 0) batches.push(current);
2127
+ current = [record];
2128
+ opened = record.at;
2129
+ } else current.push(record);
2130
+ }
2131
+ if (current.length > 0) batches.push(current);
2132
+ return batches;
2133
+ }
2134
+ function quietfor(entries, now) {
2135
+ let last = 0;
2136
+ for (const entry of entries) if (entry.responseend > last) last = entry.responseend;
2137
+ return Math.max(0, now - last);
2138
+ }
2139
+ function quietresolution(samples, idle, timeout) {
2140
+ const start = samples[0]?.at ?? 0;
2141
+ const last = samples[samples.length - 1];
2142
+ const waited = Math.max(0, (last?.at ?? 0) - start);
2143
+ const reached = samples.find((sample) => sample.quietfor >= idle);
2144
+ if (reached) return { ok: true, quietfor: reached.quietfor, waited: reached.at - start, samples: samples.length };
2145
+ return { ok: false, quietfor: last?.quietfor ?? 0, waited, samples: samples.length };
2146
+ }
2147
+ function nodehash(summary) {
2148
+ const canonical = [summary.tag, summary.text, ...Object.keys(summary.attributes).sort().map((key) => `${key}=${summary.attributes[key] ?? ""}`)].join("|");
2149
+ let hash = 5381;
2150
+ for (let index = 0; index < canonical.length; index += 1) hash = (hash << 5) + hash + canonical.charCodeAt(index) >>> 0;
2151
+ return hash.toString(16);
2152
+ }
2153
+ function diffsummaries(base, target) {
2154
+ const basemap = new Map(base.map((node) => [node.selector, node]));
2155
+ const targetmap = new Map(target.map((node) => [node.selector, node]));
2156
+ const added = [];
2157
+ const removed = [];
2158
+ const changed = [];
2159
+ for (const [selector, node] of targetmap) {
2160
+ const previous = basemap.get(selector);
2161
+ if (!previous) {
2162
+ added.push({ kind: "added", selector, summary: node.text || node.tag });
2163
+ continue;
2164
+ }
2165
+ if (nodehash(previous) !== nodehash(node)) changed.push({ kind: "changed", selector, summary: `${previous.text || previous.tag} became ${node.text || node.tag}` });
2166
+ }
2167
+ for (const [selector, node] of basemap) {
2168
+ if (!targetmap.has(selector)) removed.push({ kind: "removed", selector, summary: node.text || node.tag });
2169
+ }
2170
+ return { added, removed, changed };
2171
+ }
2172
+ function scanjson(scripts) {
2173
+ const states = [];
2174
+ let refused = 0;
2175
+ for (const script of scripts) {
2176
+ if (script.src) continue;
2177
+ const content = script.content.trim();
2178
+ if (!(script.type.includes("json") || content.startsWith("{") || content.startsWith("["))) continue;
2179
+ try {
2180
+ states.push({ scripturl: script.src, rootpath: script.id, payload: JSON.parse(content) });
2181
+ } catch {
2182
+ refused += 1;
2183
+ }
2184
+ }
2185
+ return { states, refused };
2186
+ }
2187
+ function rankselectors(shape) {
2188
+ const candidates = [];
2189
+ if (shape.id) candidates.push({ selector: `#${shape.id}`, strategy: "id", score: 100 });
2190
+ for (const [name, value] of Object.entries(shape.attributes)) {
2191
+ if (!value) continue;
2192
+ if (name === "name" || name.startsWith("data-") || name.startsWith("aria-")) candidates.push({ selector: `${shape.tag}[${name}="${value}"]`, strategy: "attribute", score: 80 });
2193
+ }
2194
+ if (shape.text) candidates.push({ selector: shape.text, strategy: "text", score: 60 });
2195
+ if (shape.index > 0) candidates.push({ selector: `${shape.tag}:nth-of-type(${shape.index})`, strategy: "structural", score: 40 });
2196
+ return candidates.sort((left, right) => right.score - left.score);
2197
+ }
2198
+ function wait3(ms) {
2199
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
2200
+ }
2201
+ function quietruleof(step) {
2202
+ let options = {};
2203
+ try {
2204
+ options = parseoptions(step);
2205
+ } catch {
2206
+ options = {};
2207
+ }
2208
+ const rule = options.quietrule;
2209
+ if (!rule || typeof rule !== "object" || Array.isArray(rule)) return { idle: 0 };
2210
+ const quiet = rule;
2211
+ return {
2212
+ idle: typeof quiet.idle === "number" && Number.isFinite(quiet.idle) && quiet.idle > 0 ? quiet.idle : 0,
2213
+ ...typeof quiet.poll === "number" && Number.isFinite(quiet.poll) && quiet.poll >= 0 ? { poll: quiet.poll } : {},
2214
+ ...typeof quiet.timeout === "number" && Number.isFinite(quiet.timeout) && quiet.timeout >= 0 ? { timeout: quiet.timeout } : {}
2215
+ };
2216
+ }
2217
+ async function watchmutations(step, root) {
2218
+ const options = parsewatchoptions(step, step.id);
2219
+ if (options.lifetime <= 0) return { ok: false, summary: "The reviewed mutation watch lifetime is absent." };
2220
+ const roots = options.scopes ? options.scopes.flatMap((selector) => [...root.querySelectorAll(selector)]) : [root];
2221
+ if (roots.length === 0) return { ok: false, summary: "The reviewed watch scopes match no elements." };
2222
+ const allowed = options.events;
2223
+ const collected = [];
2224
+ const observer = new MutationObserver((records) => {
2225
+ for (const record of records) {
2226
+ if (allowed && !allowed.includes(record.type)) continue;
2227
+ const target = record.target instanceof Element ? record.target : null;
2228
+ collected.push({ watchid: options.watchid, event: record.type, targetpath: target ? elementselector(target) : "#text", at: Date.now() });
2229
+ }
2230
+ });
2231
+ for (const scope of roots) observer.observe(scope, { childList: true, attributes: true, characterData: true, subtree: true });
2232
+ await wait3(options.lifetime);
2233
+ observer.disconnect();
2234
+ const batches = batchmutations(collected, options.poll);
2235
+ return { ok: true, summary: `Watched ${collected.length} mutation${collected.length === 1 ? "" : "s"} in ${batches.length} batch${batches.length === 1 ? "" : "es"} for the reviewed lifetime of ${options.lifetime} milliseconds.`, details: { events: collected, batches: batches.length, watchid: options.watchid, lifetime: options.lifetime, scopes: options.scopes ?? [] } };
2236
+ }
2237
+ async function watchfocus(step, root) {
2238
+ const options = parsewatchoptions(step, step.id);
2239
+ if (options.lifetime <= 0) return { ok: false, summary: "The reviewed focus watch lifetime is absent." };
2240
+ const collected = [];
2241
+ const record = (kind) => (event) => {
2242
+ const target = event.target instanceof Element ? event.target : null;
2243
+ collected.push({ watchid: options.watchid, kind, targetpath: target ? elementselector(target) : "#document", at: Date.now() });
2244
+ };
2245
+ const onfocus = record("focus");
2246
+ const onblur = record("blur");
2247
+ root.addEventListener("focusin", onfocus, true);
2248
+ root.addEventListener("focusout", onblur, true);
2249
+ await wait3(options.lifetime);
2250
+ root.removeEventListener("focusin", onfocus, true);
2251
+ root.removeEventListener("focusout", onblur, true);
2252
+ return { ok: true, summary: `Watched ${collected.length} focus change${collected.length === 1 ? "" : "s"} for the reviewed lifetime of ${options.lifetime} milliseconds.`, details: { events: collected, watchid: options.watchid, lifetime: options.lifetime } };
2253
+ }
2254
+ async function watchbanners(step, root) {
2255
+ const options = parsewatchoptions(step, step.id);
2256
+ if (options.lifetime <= 0) return { ok: false, summary: "The reviewed banner watch lifetime is absent." };
2257
+ const started = Date.now();
2258
+ const seen = /* @__PURE__ */ new Map();
2259
+ while (Date.now() - started < options.lifetime) {
2260
+ const at = Date.now();
2261
+ for (const report of bannermatches(collectbannercandidates(root), at)) {
2262
+ if (!seen.has(report.selector)) seen.set(report.selector, report);
2263
+ }
2264
+ await wait3(options.poll);
2265
+ }
2266
+ const reports = [...seen.values()];
2267
+ return { ok: true, summary: `Watched for consent banners for the reviewed lifetime of ${options.lifetime} milliseconds and observed ${reports.length} banner${reports.length === 1 ? "" : "s"}.`, details: { banners: reports, watchid: options.watchid, lifetime: options.lifetime } };
2268
+ }
2269
+ async function waitquiet(step) {
2270
+ const rule = quietruleof(step);
2271
+ if (rule.idle <= 0) return { ok: false, summary: "The reviewed quiet idle threshold is absent." };
2272
+ const poll2 = rule.poll ?? 100;
2273
+ const timeout = rule.timeout ?? 0;
2274
+ const started = performance.now();
2275
+ const samples = [];
2276
+ for (; ; ) {
2277
+ const now = performance.now();
2278
+ const entries = performance.getEntriesByType("resource").map((entry) => ({ responseend: entry.responseEnd }));
2279
+ samples.push({ at: now - started, quietfor: quietfor(entries, now) });
2280
+ const latest = samples[samples.length - 1];
2281
+ if (latest && latest.quietfor >= rule.idle) break;
2282
+ if (timeout > 0 && now - started >= timeout) break;
2283
+ await wait3(poll2);
2284
+ }
2285
+ const outcome = quietresolution(samples, rule.idle, timeout);
2286
+ return {
2287
+ ok: outcome.ok,
2288
+ summary: outcome.ok ? `The network stayed quiet for ${Math.round(outcome.quietfor)} milliseconds, meeting the reviewed idle threshold of ${rule.idle} milliseconds.` : `The network did not stay quiet for ${rule.idle} milliseconds${timeout > 0 ? ` within the reviewed timeout of ${timeout} milliseconds` : ""}.`,
2289
+ details: { samples, idle: rule.idle, timeout, waited: Math.round(outcome.waited) }
2290
+ };
2291
+ }
2292
+ function scriptsurfaces(target, root) {
2293
+ const elements = target ? [target] : [...root.querySelectorAll("script")];
2294
+ return elements.map((element) => ({ src: element.getAttribute("src") ?? "", type: element.getAttribute("type") ?? "", id: element.id, content: element.textContent ?? "" }));
2295
+ }
2296
+ function readjson(step, target, root) {
2297
+ const outcome = scanjson(scriptsurfaces(target, root));
2298
+ if (target && outcome.states.length === 0 && outcome.refused > 0) return { ok: false, summary: "The reviewed json payload is malformed and was refused." };
2299
+ return { ok: true, summary: `Extracted ${outcome.states.length} embedded json state${outcome.states.length === 1 ? "" : "s"}${outcome.refused > 0 ? ` and refused ${outcome.refused} malformed payload${outcome.refused === 1 ? "" : "s"}` : ""}.`, details: { states: outcome.states, refused: outcome.refused } };
2300
+ }
2301
+ function tonodesummaries(value) {
2302
+ if (!Array.isArray(value)) return null;
2303
+ const summaries = [];
2304
+ for (const entry of value) {
2305
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
2306
+ const candidate = entry;
2307
+ if (typeof candidate.selector !== "string") continue;
2308
+ const attributes = {};
2309
+ if (candidate.attributes && typeof candidate.attributes === "object" && !Array.isArray(candidate.attributes)) {
2310
+ for (const [key, item] of Object.entries(candidate.attributes)) if (typeof item === "string") attributes[key] = item;
2311
+ }
2312
+ summaries.push({ selector: candidate.selector, tag: typeof candidate.tag === "string" ? candidate.tag : "", text: typeof candidate.text === "string" ? candidate.text : "", attributes });
2313
+ }
2314
+ return summaries;
2315
+ }
2316
+ function diffsnapshots(step) {
2317
+ let options = {};
2318
+ try {
2319
+ options = parseoptions(step);
2320
+ } catch {
2321
+ options = {};
2322
+ }
2323
+ const base = tonodesummaries(options.base);
2324
+ const target = tonodesummaries(options.target);
2325
+ if (!base || !target) return { ok: false, summary: "Two stored observation versions must be reviewed before diffing." };
2326
+ const versions = Array.isArray(options.versions) && options.versions.length === 2 ? options.versions : [0, 0];
2327
+ const diff = diffsummaries(base, target);
2328
+ return { ok: true, summary: `Diffed observation versions ${versions[0] ?? 0} and ${versions[1] ?? 0}: ${diff.added.length} added, ${diff.removed.length} removed and ${diff.changed.length} changed node${diff.added.length + diff.removed.length + diff.changed.length === 1 ? "" : "s"}.`, details: { versions, added: diff.added, removed: diff.removed, changed: diff.changed } };
2329
+ }
2330
+ function deriveselector(target) {
2331
+ if (!(target instanceof Element)) return { ok: false, summary: "Derivation target is no longer available." };
2332
+ const attributes = {};
2333
+ for (const attribute of [...target.attributes]) attributes[attribute.name] = attribute.value;
2334
+ const parent = target.parentElement;
2335
+ const siblings = parent ? [...parent.children].filter((node) => node.tagName === target.tagName) : [target];
2336
+ const candidates = rankselectors({ id: target.id, tag: target.tagName.toLowerCase(), attributes, text: clean(target.textContent ?? "").slice(0, 80), index: siblings.indexOf(target) + 1, siblings: siblings.length });
2337
+ const best = candidates[0];
2338
+ return { ok: candidates.length > 0, summary: best ? `Derived ${candidates.length} selector candidate${candidates.length === 1 ? "" : "s"}; the most stable is ${best.selector} through the ${best.strategy} strategy with stability ${best.score}.` : "No selector candidate could be derived.", details: { candidates } };
2339
+ }
2340
+ function runpagewatch(step, target, root = document) {
2341
+ switch (step.kind) {
2342
+ case "watchmutate":
2343
+ return watchmutations(step, root);
2344
+ case "watchfocus":
2345
+ return watchfocus(step, root);
2346
+ case "watchbanner":
2347
+ return watchbanners(step, root);
2348
+ case "waitquiet":
2349
+ return waitquiet(step);
2350
+ case "readjson":
2351
+ return readjson(step, target, root);
2352
+ case "diffsnapshots":
2353
+ return diffsnapshots(step);
2354
+ case "deriveselector":
2355
+ return deriveselector(target);
2356
+ default:
2357
+ return { ok: false, summary: "Unsupported watched observation." };
2358
+ }
2359
+ }
2360
+
1139
2361
  // extension/pagebridge.ts
1140
- function stepoptions(step) {
2362
+ function stepoptions3(step) {
1141
2363
  if (!step.options) return {};
1142
2364
  try {
1143
2365
  const parsed = JSON.parse(step.options);
@@ -1177,7 +2399,26 @@
1177
2399
  ...element instanceof HTMLSelectElement ? { options: [...element.options].map((option) => clean(option.textContent || option.value)) } : {}
1178
2400
  }));
1179
2401
  const text = clean(document.body?.innerText || "");
1180
- return { schemaversion: 2, url: location.href, title: clean(document.title), textpreview: text, textlength: document.body?.innerText.length ?? 0, forms, interactive, capturedat: Date.now() };
2402
+ const tree = buildpagetree(document);
2403
+ const tables = collecttables(document).map((entry) => {
2404
+ const shape = normalizetable(entry.rows, entry.caption);
2405
+ return { selector: entry.selector, headers: shape.headers, columns: shape.columns, rows: shape.rows, caption: shape.caption };
2406
+ });
2407
+ return {
2408
+ schemaversion: 3,
2409
+ url: location.href,
2410
+ title: clean(document.title),
2411
+ textpreview: text,
2412
+ textlength: document.body?.innerText.length ?? 0,
2413
+ forms,
2414
+ interactive,
2415
+ capturedat: Date.now(),
2416
+ mode: "passive",
2417
+ a11y: builda11ytree(tree),
2418
+ reader: buildreader(tree, clean(document.title)),
2419
+ listpattern: detectlistpatterns(collectsiblings(document)),
2420
+ tableshape: tables
2421
+ };
1181
2422
  }
1182
2423
  function readdialogs() {
1183
2424
  return harvestdialoglog(document);
@@ -1220,6 +2461,10 @@
1220
2461
  var controlkinds = /* @__PURE__ */ new Set(["typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails"]);
1221
2462
  var interactkinds = /* @__PURE__ */ new Set(["clicktext", "clickaria", "clickname", "pierceshadow", "enterframe"]);
1222
2463
  var pointerkinds = /* @__PURE__ */ new Set(["movepointer", "clickpoint", "shiftclick"]);
2464
+ var observationkinds = /* @__PURE__ */ new Set(["a11ytree", "readvisible", "readertree", "readoutline", "readselection", "readopengraph", "readlang", "detectlanguage", "listshadow", "listframes"]);
2465
+ var detectionkinds = /* @__PURE__ */ new Set(["detectlists", "detecttables", "detectinfinitescroll", "detectvirtual", "detectlazy", "detectsticky", "detectscrolllock", "countpages", "classifypage", "fingerprintsection", "readscrollpos"]);
2466
+ var watchstepkinds = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "waitquiet", "readjson", "diffsnapshots", "deriveselector"]);
2467
+ var navstepkinds = /* @__PURE__ */ new Set(["waitload", "waiturl", "followlink", "spanav", "spawait", "rewritequery", "setfragment", "stopnav", "prefetch", "preconnect", "printpdf"]);
1223
2468
  async function performstep(step, expectedorigin, rootdocument = document) {
1224
2469
  if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before action." };
1225
2470
  if (step.kind === "observe") return { ok: true, summary: "Observation completed." };
@@ -1246,8 +2491,9 @@
1246
2491
  history.forward();
1247
2492
  return { ok: true, summary: "History forward requested." };
1248
2493
  }
2494
+ if (navstepkinds.has(step.kind)) return await runpagenav(step, rootdocument);
1249
2495
  if (step.kind === "scrollpage") {
1250
- const options = stepoptions(step);
2496
+ const options = stepoptions3(step);
1251
2497
  window.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
1252
2498
  return { ok: true, summary: "Window scrolled by the reviewed amounts." };
1253
2499
  }
@@ -1269,11 +2515,14 @@
1269
2515
  else if (controlkinds.has(step.kind)) result = runpagecontrol(step, element, rootdocument);
1270
2516
  else if (interactkinds.has(step.kind)) return await runinteractstep(step, expectedorigin, performstep);
1271
2517
  else if (pointerkinds.has(step.kind)) result = runpointerstep(step, resolution);
2518
+ else if (observationkinds.has(step.kind)) result = runpageobservation(step, element, rootdocument);
2519
+ else if (detectionkinds.has(step.kind)) result = runpagedetection(step, element, rootdocument);
2520
+ else if (watchstepkinds.has(step.kind)) result = runpagewatch(step, element, rootdocument);
1272
2521
  else if (mutatingkinds.has(step.kind)) result = runpageaction(step, element);
1273
2522
  else {
1274
2523
  if (!element) return { ok: false, summary: "Action target is no longer available." };
1275
2524
  if (step.kind === "scrollby") {
1276
- const options = stepoptions(step);
2525
+ const options = stepoptions3(step);
1277
2526
  element.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
1278
2527
  result = { ok: true, summary: "Container scrolled by the reviewed amounts." };
1279
2528
  } else if (step.kind === "focus") {