@wenathlan/extension 1.1.33 → 1.1.34

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.
@@ -3,7 +3,7 @@
3
3
  // policy.ts
4
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"]);
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"]);
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) {
@@ -1136,8 +1149,900 @@
1136
1149
  }
1137
1150
  }
1138
1151
 
1139
- // extension/pagebridge.ts
1152
+ // extension/pageobserve.ts
1153
+ var maxframedepth = 4;
1154
+ function elementstates(element) {
1155
+ const states = [];
1156
+ if (element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true") states.push("disabled");
1157
+ if (element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio") && element.checked) states.push("checked");
1158
+ const expanded = element.getAttribute("aria-expanded");
1159
+ if (expanded !== null) states.push(`expanded ${expanded}`);
1160
+ if (element.getAttribute("aria-selected") === "true") states.push("selected");
1161
+ if (element.hasAttribute("required") || element.getAttribute("aria-required") === "true") states.push("required");
1162
+ if (element.hasAttribute("readonly") || element.getAttribute("aria-readonly") === "true") states.push("readonly");
1163
+ if (element.getAttribute("aria-hidden") === "true") states.push("hidden");
1164
+ return states;
1165
+ }
1166
+ function elementvalue(element) {
1167
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) return element.value;
1168
+ return "";
1169
+ }
1170
+ function elementhidden(element) {
1171
+ if (element instanceof HTMLInputElement && element.type === "hidden") return true;
1172
+ if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true") return true;
1173
+ try {
1174
+ const style = element.ownerDocument?.defaultView?.getComputedStyle(element);
1175
+ if (style && (style.display === "none" || style.visibility === "hidden")) return true;
1176
+ } catch {
1177
+ }
1178
+ return false;
1179
+ }
1180
+ function framenode(frame, depth) {
1181
+ let content = null;
1182
+ try {
1183
+ content = frame.contentDocument;
1184
+ } catch {
1185
+ content = null;
1186
+ }
1187
+ let sameorigin = false;
1188
+ try {
1189
+ sameorigin = content !== null && frame.contentWindow?.location.origin === location.origin;
1190
+ } catch {
1191
+ sameorigin = false;
1192
+ }
1193
+ const node = wrap2(frame, depth);
1194
+ if (sameorigin && content && depth < maxframedepth) node.children.push(...wrapchildren(content, depth + 1));
1195
+ return node;
1196
+ }
1197
+ function wrap2(element, depth) {
1198
+ const shadow = element.shadowRoot;
1199
+ const node = {
1200
+ tag: element.tagName.toLowerCase(),
1201
+ selector: elementselector(element),
1202
+ id: element.id,
1203
+ classes: [...element.classList],
1204
+ role: element.getAttribute("role")?.toLowerCase() || implicitrole(element),
1205
+ name: elementlabel(element),
1206
+ text: owntext2(element),
1207
+ value: elementvalue(element),
1208
+ states: elementstates(element),
1209
+ hidden: elementhidden(element),
1210
+ children: [],
1211
+ element
1212
+ };
1213
+ if (shadow) node.children.push(...wrapchildren(shadow, depth));
1214
+ if (element instanceof HTMLIFrameElement) return framenode(element, depth);
1215
+ node.children.push(...wrapchildren(element, depth));
1216
+ return node;
1217
+ }
1218
+ function wrapchildren(scope, depth) {
1219
+ return [...scope.querySelectorAll(":scope > *")].map((child) => wrap2(child, depth));
1220
+ }
1221
+ function buildpagetree(scope) {
1222
+ const root = {
1223
+ tag: "#document",
1224
+ selector: "",
1225
+ id: "",
1226
+ classes: [],
1227
+ role: "document",
1228
+ name: "",
1229
+ text: "",
1230
+ value: "",
1231
+ states: [],
1232
+ hidden: false,
1233
+ children: []
1234
+ };
1235
+ if (scope instanceof Document) {
1236
+ root.children = scope.documentElement ? [wrap2(scope.documentElement, 0)] : [];
1237
+ } else {
1238
+ root.children = [...wrapchildren(scope, 0)];
1239
+ }
1240
+ return root;
1241
+ }
1242
+ function countnodes(node) {
1243
+ return 1 + node.children.reduce((total, child) => total + countnodes(child), 0);
1244
+ }
1245
+ function builda11ytree(node) {
1246
+ const children = node.children.filter((child) => !child.hidden).map(builda11ytree);
1247
+ return {
1248
+ role: node.role || "generic",
1249
+ name: node.name,
1250
+ states: node.states,
1251
+ ...node.value ? { value: node.value } : {},
1252
+ childcount: children.length,
1253
+ children
1254
+ };
1255
+ }
1256
+ function visibleentries(node) {
1257
+ if (node.hidden) return [];
1258
+ const entries = node.text ? [{ selector: node.selector || node.tag, text: node.text }] : [];
1259
+ for (const child of node.children) entries.push(...visibleentries(child));
1260
+ return entries;
1261
+ }
1262
+ function visibletext(node) {
1263
+ return visibleentries(node).map((entry) => entry.text).join(" ");
1264
+ }
1265
+ function nodetextlength(node) {
1266
+ return node.text.length + node.children.reduce((total, child) => total + nodetextlength(child), 0);
1267
+ }
1268
+ function nodelinktext(node) {
1269
+ const own = node.tag === "a" ? node.text.length : 0;
1270
+ return own + node.children.reduce((total, child) => total + nodelinktext(child), 0);
1271
+ }
1272
+ function wordsin(text) {
1273
+ return text.split(/\s+/).filter(Boolean).length;
1274
+ }
1275
+ function findbyline(node) {
1276
+ const markers = ["byline", "author"];
1277
+ const direct = node.classes.some((item) => markers.some((marker) => item.toLowerCase().includes(marker))) || markers.some((marker) => node.id.toLowerCase().includes(marker));
1278
+ if (direct && node.text) return node.text;
1279
+ for (const child of node.children) {
1280
+ const found = findbyline(child);
1281
+ if (found) return found;
1282
+ }
1283
+ return "";
1284
+ }
1285
+ function findheading(node, tags) {
1286
+ if (tags.includes(node.tag) && node.text) return node.text;
1287
+ for (const child of node.children) {
1288
+ const found = findheading(child, tags);
1289
+ if (found) return found;
1290
+ }
1291
+ return "";
1292
+ }
1293
+ function buildreader(root, title) {
1294
+ let best;
1295
+ let bestscore = 0;
1296
+ const walk = (node) => {
1297
+ if (node.tag !== "#document") {
1298
+ const length = nodetextlength(node);
1299
+ const links = nodelinktext(node);
1300
+ const score = length * (1 - (length > 0 ? links / length : 0));
1301
+ if (score > bestscore) {
1302
+ bestscore = score;
1303
+ best = node;
1304
+ }
1305
+ }
1306
+ for (const child of node.children) walk(child);
1307
+ };
1308
+ walk(root);
1309
+ const article = best ?? root;
1310
+ const blocks = article.children.filter((child) => !child.hidden && child.text).map((child) => ({ kind: child.tag, text: child.text, words: wordsin(child.text) }));
1311
+ const ownblock = article.text ? [{ kind: article.tag, text: article.text, words: wordsin(article.text) }] : [];
1312
+ const allblocks = [...ownblock, ...blocks];
1313
+ return {
1314
+ title: findheading(article, ["h1"]) || findheading(root, ["h1"]) || title,
1315
+ byline: findbyline(article) || findbyline(root),
1316
+ blocks: allblocks,
1317
+ words: allblocks.reduce((total, block) => total + block.words, 0),
1318
+ characters: allblocks.reduce((total, block) => total + block.text.length, 0)
1319
+ };
1320
+ }
1321
+ function pageoutline(root, title) {
1322
+ const headings = [];
1323
+ const walk = (node) => {
1324
+ const level = /^h([1-6])$/.exec(node.tag);
1325
+ if (level && node.text) headings.push({ level: Number.parseInt(level[1], 10), text: node.text });
1326
+ for (const child of node.children) walk(child);
1327
+ };
1328
+ walk(root);
1329
+ return { title: findheading(root, ["h1"]) || title, headings };
1330
+ }
1331
+ function captureselection(root) {
1332
+ const selection = root.getSelection?.() ?? null;
1333
+ const text = selection ? clean(selection.toString()) : "";
1334
+ return { text, length: text.length };
1335
+ }
1336
+ function opengraphfields(meta, jsonld) {
1337
+ const graph = {};
1338
+ for (const entry of meta) {
1339
+ if (entry.property.startsWith("og:") && entry.content) graph[entry.property] = entry.content;
1340
+ }
1341
+ const structured = [];
1342
+ let refused = 0;
1343
+ for (const raw of jsonld) {
1344
+ try {
1345
+ structured.push(JSON.parse(raw));
1346
+ } catch {
1347
+ refused += 1;
1348
+ }
1349
+ }
1350
+ return { graph, structured, refused };
1351
+ }
1352
+ var stopwords = {
1353
+ en: ["the", "is", "at", "which", "on", "and", "of", "to", "in", "that", "it", "with"],
1354
+ pt: ["de", "que", "n\xE3o", "uma", "para", "com", "por", "mais", "como", "p\xE1gina", "este", "voc\xEA"],
1355
+ es: ["que", "el", "las", "los", "por", "una", "para", "con", "como", "p\xE1gina", "m\xE1s", "este"],
1356
+ fr: ["le", "les", "des", "que", "pour", "dans", "est", "sur", "avec", "page", "plus", "cette"],
1357
+ de: ["der", "die", "und", "das", "ist", "von", "mit", "f\xFCr", "auf", "den", "nicht", "seite"],
1358
+ it: ["che", "il", "la", "per", "una", "del", "sono", "non", "con", "pagina", "pi\xF9", "questo"],
1359
+ nl: ["het", "een", "en", "van", "is", "dat", "op", "te", "voor", "met", "niet", "pagina"]
1360
+ };
1361
+ function detecttextlanguage(text) {
1362
+ const words = text.toLowerCase().split(/[^a-zà-ÿ]+/).filter(Boolean);
1363
+ if (words.length === 0) return "";
1364
+ let best = "";
1365
+ let bestscore = 0;
1366
+ for (const [language, dictionary] of Object.entries(stopwords)) {
1367
+ const score = words.filter((word) => dictionary.includes(word)).length;
1368
+ if (score > bestscore) {
1369
+ bestscore = score;
1370
+ best = language;
1371
+ }
1372
+ }
1373
+ return best;
1374
+ }
1375
+ function taglanguage(text) {
1376
+ return { text, language: detecttextlanguage(text) };
1377
+ }
1378
+ function documentlanguage(signals) {
1379
+ if (signals.lang.trim()) return { language: signals.lang.trim(), source: "document" };
1380
+ if (signals.meta.trim()) return { language: signals.meta.trim(), source: "meta" };
1381
+ return { language: detecttextlanguage(signals.text), source: "content" };
1382
+ }
1383
+ function shadowpaths(scope) {
1384
+ const paths = [];
1385
+ const walk = (tree, prefix) => {
1386
+ for (const shadow of tree.shadows) {
1387
+ if (!shadow.host) continue;
1388
+ const path = prefix ? `${prefix} > ${shadow.host.selector}` : shadow.host.selector;
1389
+ paths.push(path);
1390
+ walk(shadow, path);
1391
+ }
1392
+ };
1393
+ walk(scope, "");
1394
+ return paths;
1395
+ }
1396
+ function framelist(root) {
1397
+ return [...root.querySelectorAll("iframe")].map((frame, index) => {
1398
+ let origin = "";
1399
+ try {
1400
+ origin = frame.contentWindow?.location.origin ?? "";
1401
+ } catch {
1402
+ origin = "";
1403
+ }
1404
+ const rect = frame.getBoundingClientRect();
1405
+ return { index, origin, sameorigin: origin !== "" && origin === location.origin, width: Math.round(rect.width), height: Math.round(rect.height) };
1406
+ });
1407
+ }
1140
1408
  function stepoptions(step) {
1409
+ try {
1410
+ return parseoptions(step);
1411
+ } catch {
1412
+ return {};
1413
+ }
1414
+ }
1415
+ function runpageobservation(step, target, root = document) {
1416
+ switch (step.kind) {
1417
+ case "a11ytree": {
1418
+ const tree = builda11ytree(buildpagetree(root));
1419
+ const count = countnodes(tree);
1420
+ return { ok: true, summary: `Captured the accessibility tree with ${count} node${count === 1 ? "" : "s"}.`, details: { tree, nodecount: count } };
1421
+ }
1422
+ case "readvisible": {
1423
+ const scope = target ?? root;
1424
+ const tree = buildpagetree(scope);
1425
+ const entries = visibleentries(tree);
1426
+ return { ok: true, summary: `Read the rendered text of ${entries.length} visible element${entries.length === 1 ? "" : "s"}.`, details: { entries, text: visibletext(tree) } };
1427
+ }
1428
+ case "readertree": {
1429
+ const article = buildreader(buildpagetree(root), root.title);
1430
+ return { ok: true, summary: `Extracted the reader view with ${article.blocks.length} block${article.blocks.length === 1 ? "" : "s"} and ${article.words} words.`, details: { article } };
1431
+ }
1432
+ case "readoutline": {
1433
+ const outline = pageoutline(buildpagetree(root), root.title);
1434
+ return { ok: true, summary: `Read the outline with ${outline.headings.length} heading${outline.headings.length === 1 ? "" : "s"}.`, details: { title: outline.title, headings: outline.headings } };
1435
+ }
1436
+ case "readselection": {
1437
+ const selection = captureselection(root);
1438
+ 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 } };
1439
+ }
1440
+ case "readopengraph": {
1441
+ const meta = [...root.querySelectorAll("meta")].map((element) => ({ property: element.getAttribute("property") ?? "", name: element.getAttribute("name") ?? "", content: element.getAttribute("content") ?? "" }));
1442
+ const jsonld = [...root.querySelectorAll('script[type="application/ld+json"]')].map((element) => element.textContent ?? "");
1443
+ const fields = opengraphfields(meta, jsonld);
1444
+ 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 } };
1445
+ }
1446
+ case "readlang": {
1447
+ const metatag = root.querySelector('meta[http-equiv="content-language"]')?.getAttribute("content") ?? "";
1448
+ const outcome = documentlanguage({ lang: root.documentElement?.getAttribute("lang") ?? "", meta: metatag, text: root.body?.innerText ?? "" });
1449
+ return { ok: true, summary: `Detected page language ${outcome.language || "unknown"} from the ${outcome.source} signal.`, details: { language: outcome.language, source: outcome.source } };
1450
+ }
1451
+ case "detectlanguage": {
1452
+ const options = stepoptions(step);
1453
+ const text = typeof options.text === "string" && options.text ? options.text : target?.textContent ?? root.body?.innerText ?? "";
1454
+ const routed = taglanguage(clean(text));
1455
+ 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 } };
1456
+ }
1457
+ case "listshadow": {
1458
+ const paths = shadowpaths(describescopes(root));
1459
+ return { ok: true, summary: `Listed ${paths.length} open shadow root${paths.length === 1 ? "" : "s"}.`, details: { shadows: paths } };
1460
+ }
1461
+ case "listframes": {
1462
+ const frames = framelist(root);
1463
+ return { ok: true, summary: `Listed ${frames.length} iframe${frames.length === 1 ? "" : "s"}.`, details: { frames } };
1464
+ }
1465
+ default:
1466
+ return { ok: false, summary: "Unsupported page observation." };
1467
+ }
1468
+ }
1469
+
1470
+ // extension/pagedetect.ts
1471
+ function detectlistpatterns(samples) {
1472
+ const patterns = [];
1473
+ for (const sample of samples) {
1474
+ const groups = /* @__PURE__ */ new Map();
1475
+ for (const child of sample.children) {
1476
+ const key = `${child.tag}|${child.classes}`;
1477
+ const group = groups.get(key) ?? [];
1478
+ group.push(child);
1479
+ groups.set(key, group);
1480
+ }
1481
+ for (const [key, group] of groups) {
1482
+ if (group.length < 2) continue;
1483
+ if (!group.some((item) => item.text)) continue;
1484
+ const [tag, classes] = key.split("|");
1485
+ const classpart = (classes ?? "").split(" ").filter(Boolean).map((name) => `.${name}`).join("");
1486
+ patterns.push({ container: sample.container, itemselector: `${tag}${classpart}`, repeat: group.length, samples: group.map((item) => item.text).filter(Boolean) });
1487
+ }
1488
+ }
1489
+ return patterns;
1490
+ }
1491
+ function normalizetable(rows, caption) {
1492
+ const firstheader = rows.find((row) => row.header);
1493
+ const headers = firstheader?.cells ?? [];
1494
+ const body = firstheader ? rows.filter((row) => row !== firstheader) : rows;
1495
+ const width = rows.reduce((largest, row) => Math.max(largest, row.cells.length), 0);
1496
+ const columns = [];
1497
+ for (let index = 0; index < width; index += 1) {
1498
+ const label = headers[index] ?? `column ${index + 1}`;
1499
+ const cells = body.filter((row) => Boolean((row.cells[index] ?? "").trim())).length;
1500
+ columns.push({ label, cells });
1501
+ }
1502
+ return { headers, columns, rows: body.length, caption };
1503
+ }
1504
+ function paginationestimate(entries) {
1505
+ const pages = [];
1506
+ let current = 0;
1507
+ for (const entry of entries) {
1508
+ const parsed = /^\d+$/.exec(entry.text.trim());
1509
+ if (parsed) {
1510
+ const page = Number.parseInt(parsed[0], 10);
1511
+ pages.push(page);
1512
+ if (entry.current) current = page;
1513
+ }
1514
+ }
1515
+ const total = Math.max(0, ...pages, current);
1516
+ return { current, total, links: entries.length, pages };
1517
+ }
1518
+ function infinitescrollranges(ranges) {
1519
+ 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 }));
1520
+ }
1521
+ function virtualizedcontainers(containers) {
1522
+ const results = [];
1523
+ for (const container of containers) {
1524
+ const first = container.rows[0];
1525
+ if (!first || container.rows.length < 2 || first.height <= 0) continue;
1526
+ if (!container.rows.every((row) => row.height === first.height)) continue;
1527
+ if (container.scrollheight <= container.rows.length * first.height) continue;
1528
+ results.push({ selector: container.selector, rendered: container.rows.length, estimated: Math.floor(container.scrollheight / first.height) });
1529
+ }
1530
+ return results;
1531
+ }
1532
+ function lazysurvey(images) {
1533
+ const lazy = [];
1534
+ const placeholders = [];
1535
+ for (const image of images) {
1536
+ if (image.loading === "lazy") lazy.push({ selector: image.selector, reason: "loading attribute" });
1537
+ else if (image.datasrc) lazy.push({ selector: image.selector, reason: "deferred source" });
1538
+ if (!image.src) placeholders.push({ selector: image.selector, reason: "empty source" });
1539
+ else if (image.src.startsWith("data:")) placeholders.push({ selector: image.selector, reason: "inline data placeholder" });
1540
+ }
1541
+ return { lazy, placeholders };
1542
+ }
1543
+ function overlaygeometry(elements, viewport) {
1544
+ const area = viewport.width * viewport.height;
1545
+ return elements.filter((element) => (element.position === "sticky" || element.position === "fixed") && element.top <= 0 && element.height > 0).map((element) => {
1546
+ const coverage = area > 0 ? element.height * element.width / area : 0;
1547
+ return { selector: element.selector, position: element.position, coverage: Math.round(coverage * 1e3) / 1e3, hides: coverage >= overlaythreshold };
1548
+ });
1549
+ }
1550
+ function scrolllockstate(signals) {
1551
+ const reasons = [];
1552
+ if (signals.bodyoverflow.includes("hidden") || signals.htmloverflow.includes("hidden")) reasons.push("overflow hidden");
1553
+ if (signals.bodyposition === "fixed") reasons.push("fixed body");
1554
+ if (signals.modal) reasons.push("modal open");
1555
+ return { locked: reasons.length > 0, reasons, scrollable: signals.scrollable };
1556
+ }
1557
+ var consentkeywords = ["cookie", "consent", "gdpr", "lgpd", "privacy", "ccpa"];
1558
+ function bannermatches(candidates, at) {
1559
+ const reports = [];
1560
+ for (const candidate of candidates) {
1561
+ const haystack = `${candidate.id} ${candidate.classes.join(" ")} ${candidate.text}`.toLowerCase();
1562
+ const keyword = consentkeywords.find((word) => haystack.includes(word));
1563
+ if (!keyword) continue;
1564
+ if (!candidate.text && candidate.controls.length === 0) continue;
1565
+ reports.push({ kind: keyword, selector: candidate.selector, text: candidate.text.slice(0, 200), controls: candidate.controls, at });
1566
+ }
1567
+ return reports;
1568
+ }
1569
+ function classifytemplate(signals) {
1570
+ if (signals.password) return "login";
1571
+ if (signals.paragraphs >= 3) return "article";
1572
+ if (signals.tables > 0) return "table";
1573
+ if (signals.forms > 0 && signals.inputs > 0) return "form";
1574
+ if (signals.lists > 0) return "list";
1575
+ return "generic";
1576
+ }
1577
+ function sectionfingerprint(section) {
1578
+ const canonical = [section.tag, String(section.children), String(section.textlength), ...Object.keys(section.attributes).sort().map((key) => `${key}=${section.attributes[key] ?? ""}`)].join("|");
1579
+ let hash = 5381;
1580
+ for (let index = 0; index < canonical.length; index += 1) hash = (hash << 5) + hash + canonical.charCodeAt(index) >>> 0;
1581
+ return `fp${hash.toString(16)}`;
1582
+ }
1583
+ function scrollreport(window2, containers) {
1584
+ const range = Math.max(0, window2.scrollheight - window2.clientheight);
1585
+ return {
1586
+ window: { x: window2.scrollx, y: window2.scrolly, attop: window2.scrolly <= 0, atbottom: window2.scrolly >= range, height: window2.scrollheight },
1587
+ containers: containers.map((container) => {
1588
+ const containerrange = Math.max(0, container.scrollheight - container.clientheight);
1589
+ return { selector: container.selector, scrolltop: container.scrolltop, scrollleft: container.scrollleft, scrollrange: containerrange, atbottom: container.scrolltop >= containerrange };
1590
+ })
1591
+ };
1592
+ }
1593
+ var loadmorepattern = /(load more|show more|see more|ver mais|carregar mais|load older|afficher plus|mehr anzeigen)/i;
1594
+ var paginationtext = /^(next|prev|previous|last|first|next page|previous page|»|«|›|‹|\d+)$/i;
1595
+ var overlaythreshold = 0.25;
1596
+ function signatureof(element) {
1597
+ return `${element.tagName.toLowerCase()}|${[...element.classList].sort().join(" ")}`;
1598
+ }
1599
+ function collectsiblings(root) {
1600
+ const samples = [];
1601
+ for (const element of [...root.querySelectorAll("*")]) {
1602
+ const children = [...element.children];
1603
+ if (children.length < 2) continue;
1604
+ const counts = /* @__PURE__ */ new Map();
1605
+ for (const child of children) {
1606
+ const key = signatureof(child);
1607
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1608
+ }
1609
+ if (![...counts.values()].some((count) => count >= 2)) continue;
1610
+ samples.push({
1611
+ container: elementselector(element),
1612
+ children: children.map((child) => ({ tag: child.tagName.toLowerCase(), classes: [...child.classList].sort().join(" "), text: clean(child.textContent ?? ""), selector: elementselector(child) }))
1613
+ });
1614
+ }
1615
+ return samples;
1616
+ }
1617
+ function collecttables(root) {
1618
+ return [...root.querySelectorAll("table")].map((table) => ({
1619
+ selector: elementselector(table),
1620
+ rows: [...table.querySelectorAll("tr")].map((row) => ({ cells: [...row.querySelectorAll("th, td")].map((cell) => clean(cell.textContent ?? "")), header: Boolean(row.querySelector("th")) })),
1621
+ caption: clean(table.querySelector("caption")?.textContent ?? "")
1622
+ }));
1623
+ }
1624
+ function collectpagination(root) {
1625
+ const entries = [];
1626
+ for (const element of [...root.querySelectorAll("a[href], button, [role=button], [role=link], li, span")]) {
1627
+ const text = clean(element.textContent ?? "");
1628
+ if (!text || !paginationtext.test(text)) continue;
1629
+ if (!element.closest("nav, footer, [class*=pag i], [id*=pag i]")) continue;
1630
+ const current = element.getAttribute("aria-current") === "page" || [...element.classList].some((name) => /current|active|selecionado/i.test(name));
1631
+ entries.push({ text, selector: elementselector(element), current });
1632
+ }
1633
+ return entries;
1634
+ }
1635
+ function collecttriggers(scope) {
1636
+ const triggers = [];
1637
+ for (const element of [...scope.querySelectorAll("button, a[href], [role=button], [class*=loading i], [class*=sentinel i], [class*=spinner i]")]) {
1638
+ const label = clean(element.getAttribute("aria-label") ?? element.textContent ?? "");
1639
+ if (loadmorepattern.test(label)) triggers.push(elementselector(element));
1640
+ }
1641
+ return triggers;
1642
+ }
1643
+ function collectscrollranges(root) {
1644
+ const ranges = [];
1645
+ const scrolling = root.scrollingElement ?? root.documentElement;
1646
+ const viewheight = root.defaultView?.innerHeight ?? 0;
1647
+ if (scrolling && scrolling.scrollHeight > viewheight) ranges.push({ selector: "window", scrollheight: scrolling.scrollHeight, clientheight: viewheight, triggers: collecttriggers(root) });
1648
+ for (const element of [...root.querySelectorAll("*")]) {
1649
+ if (!(element instanceof HTMLElement)) continue;
1650
+ if (element.scrollHeight <= element.clientHeight) continue;
1651
+ ranges.push({ selector: elementselector(element), scrollheight: element.scrollHeight, clientheight: element.clientHeight, triggers: collecttriggers(element) });
1652
+ }
1653
+ return ranges;
1654
+ }
1655
+ function collectvirtual(root) {
1656
+ const containers = [];
1657
+ for (const element of [...root.querySelectorAll("*")]) {
1658
+ const children = [...element.children];
1659
+ const first = children[0];
1660
+ if (!first || children.length < 2) continue;
1661
+ if (!children.every((child) => signatureof(child) === signatureof(first))) continue;
1662
+ const heights = children.map((child) => child.getBoundingClientRect().height);
1663
+ if (!heights.every((height) => height > 0 && height === heights[0])) continue;
1664
+ containers.push({ selector: elementselector(element), scrollheight: element.scrollHeight, rows: children.map((child) => ({ selector: elementselector(child), height: child.getBoundingClientRect().height, classes: [...child.classList].join(" ") })) });
1665
+ }
1666
+ return containers;
1667
+ }
1668
+ function collectimages(root) {
1669
+ return [...root.querySelectorAll("img")].map((image) => ({
1670
+ selector: elementselector(image),
1671
+ src: image.getAttribute("src") ?? "",
1672
+ datasrc: image.getAttribute("data-src") ?? image.getAttribute("data-original") ?? "",
1673
+ loading: image.getAttribute("loading") ?? "",
1674
+ width: image.naturalWidth,
1675
+ height: image.naturalHeight
1676
+ }));
1677
+ }
1678
+ function collectoverlays(root) {
1679
+ const elements = [];
1680
+ for (const element of [...root.querySelectorAll("*")]) {
1681
+ if (!(element instanceof HTMLElement)) continue;
1682
+ const view = element.ownerDocument.defaultView;
1683
+ const position = view ? view.getComputedStyle(element).position : "";
1684
+ if (position !== "sticky" && position !== "fixed") continue;
1685
+ const rect = element.getBoundingClientRect();
1686
+ elements.push({ selector: elementselector(element), position, top: rect.top, height: rect.height, width: rect.width });
1687
+ }
1688
+ return elements;
1689
+ }
1690
+ function collectlocksignals(root) {
1691
+ const view = root.defaultView;
1692
+ const bodystyle = root.body ? view ? view.getComputedStyle(root.body) : void 0 : void 0;
1693
+ const htmlstyle = view ? view.getComputedStyle(root.documentElement) : void 0;
1694
+ return {
1695
+ bodyoverflow: bodystyle?.overflow ?? "",
1696
+ htmloverflow: htmlstyle?.overflow ?? "",
1697
+ bodyposition: bodystyle?.position ?? "",
1698
+ modal: Boolean(root.querySelector("dialog[open], [aria-modal=true]")),
1699
+ scrollable: root.documentElement.scrollHeight > root.documentElement.clientHeight
1700
+ };
1701
+ }
1702
+ 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"]';
1703
+ function collectbannercandidates(root) {
1704
+ const found = [...root.querySelectorAll(bannerselector)];
1705
+ return found.filter((element) => !found.some((other) => other !== element && other.contains(element))).map((element) => ({
1706
+ selector: elementselector(element),
1707
+ id: element.id,
1708
+ classes: [...element.classList],
1709
+ text: clean(element.textContent ?? "").slice(0, 200),
1710
+ controls: [...element.querySelectorAll("button, a[href], [role=button]")].map((control) => clean(control.getAttribute("aria-label") ?? control.textContent ?? "")).filter(Boolean)
1711
+ }));
1712
+ }
1713
+ function runpagedetection(step, target, root = document) {
1714
+ switch (step.kind) {
1715
+ case "detectlists": {
1716
+ const patterns = detectlistpatterns(collectsiblings(root));
1717
+ return { ok: true, summary: `Detected ${patterns.length} repeated list${patterns.length === 1 ? "" : "s"}.`, details: { lists: patterns } };
1718
+ }
1719
+ case "detecttables": {
1720
+ const tables = collecttables(root).map((entry) => {
1721
+ const shape = normalizetable(entry.rows, entry.caption);
1722
+ return { selector: entry.selector, headers: shape.headers, columns: shape.columns, rows: shape.rows, caption: shape.caption };
1723
+ });
1724
+ return { ok: true, summary: `Detected ${tables.length} data table${tables.length === 1 ? "" : "s"}.`, details: { tables } };
1725
+ }
1726
+ case "countpages": {
1727
+ const estimate = paginationestimate(collectpagination(root));
1728
+ 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 } };
1729
+ }
1730
+ case "detectinfinitescroll": {
1731
+ const containers = infinitescrollranges(collectscrollranges(root));
1732
+ return { ok: true, summary: `Detected ${containers.length} infinite scroll container${containers.length === 1 ? "" : "s"}.`, details: { containers } };
1733
+ }
1734
+ case "detectvirtual": {
1735
+ const containers = virtualizedcontainers(collectvirtual(root));
1736
+ return { ok: true, summary: `Detected ${containers.length} virtualized list${containers.length === 1 ? "" : "s"}.`, details: { containers } };
1737
+ }
1738
+ case "detectlazy": {
1739
+ const survey = lazysurvey(collectimages(root));
1740
+ 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 } };
1741
+ }
1742
+ case "detectsticky": {
1743
+ const overlays = overlaygeometry(collectoverlays(root), { width: root.defaultView?.innerWidth ?? 0, height: root.defaultView?.innerHeight ?? 0 });
1744
+ return { ok: true, summary: `Detected ${overlays.length} sticky or fixed overlay${overlays.length === 1 ? "" : "s"}.`, details: { overlays } };
1745
+ }
1746
+ case "detectscrolllock": {
1747
+ const lock = scrolllockstate(collectlocksignals(root));
1748
+ 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 } };
1749
+ }
1750
+ case "classifypage": {
1751
+ const signals = {
1752
+ paragraphs: root.querySelectorAll("p").length,
1753
+ headings: root.querySelectorAll("h1, h2, h3, h4, h5, h6").length,
1754
+ lists: root.querySelectorAll("ul, ol").length,
1755
+ tables: root.querySelectorAll("table").length,
1756
+ forms: root.querySelectorAll("form").length,
1757
+ inputs: root.querySelectorAll("input, textarea, select").length,
1758
+ password: Boolean(root.querySelector("input[type=password]"))
1759
+ };
1760
+ const template = classifytemplate(signals);
1761
+ const fingerprint = sectionfingerprint({ tag: "body", attributes: {}, children: root.body?.children.length ?? 0, textlength: (root.body?.innerText ?? "").length });
1762
+ return { ok: true, summary: `Classified the page template as ${template}.`, details: { template, fingerprint } };
1763
+ }
1764
+ case "fingerprintsection": {
1765
+ if (!target) return { ok: false, summary: "Fingerprint target is no longer available." };
1766
+ const attributes = {};
1767
+ for (const attribute of [...target.attributes]) attributes[attribute.name] = attribute.value;
1768
+ const fingerprint = sectionfingerprint({ tag: target.tagName.toLowerCase(), attributes, children: target.children.length, textlength: (target.textContent ?? "").length });
1769
+ return { ok: true, summary: `Computed section fingerprint ${fingerprint}.`, details: { fingerprint, section: elementselector(target) } };
1770
+ }
1771
+ case "readscrollpos": {
1772
+ const report = scrollreport(
1773
+ { scrollx: root.defaultView?.scrollX ?? 0, scrolly: root.defaultView?.scrollY ?? 0, scrollheight: root.documentElement.scrollHeight, clientheight: root.defaultView?.innerHeight ?? 0 },
1774
+ [...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 }))
1775
+ );
1776
+ 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 } };
1777
+ }
1778
+ default:
1779
+ return { ok: false, summary: "Unsupported page detection." };
1780
+ }
1781
+ }
1782
+
1783
+ // extension/pagewatch.ts
1784
+ var defaultpoll = 250;
1785
+ function parsewatchoptions(step, fallbackid) {
1786
+ let options = {};
1787
+ try {
1788
+ options = parseoptions(step);
1789
+ } catch {
1790
+ options = {};
1791
+ }
1792
+ const scopes = Array.isArray(options.scopes) ? options.scopes.filter((item) => typeof item === "string" && item.trim().length > 0) : void 0;
1793
+ const events3 = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : void 0;
1794
+ const lifetime = typeof options.lifetime === "number" && Number.isFinite(options.lifetime) && options.lifetime > 0 ? options.lifetime : 0;
1795
+ return {
1796
+ watchid: typeof options.watchid === "string" && options.watchid.trim() ? options.watchid : fallbackid,
1797
+ ...scopes ? { scopes } : {},
1798
+ ...events3 ? { events: events3 } : {},
1799
+ lifetime,
1800
+ poll: typeof options.poll === "number" && Number.isFinite(options.poll) && options.poll >= 0 ? options.poll : defaultpoll
1801
+ };
1802
+ }
1803
+ function batchmutations(records, windowms) {
1804
+ const batches = [];
1805
+ let current = [];
1806
+ let opened = -1;
1807
+ for (const record of records) {
1808
+ if (current.length === 0 || windowms > 0 && record.at - opened >= windowms) {
1809
+ if (current.length > 0) batches.push(current);
1810
+ current = [record];
1811
+ opened = record.at;
1812
+ } else current.push(record);
1813
+ }
1814
+ if (current.length > 0) batches.push(current);
1815
+ return batches;
1816
+ }
1817
+ function quietfor(entries, now) {
1818
+ let last = 0;
1819
+ for (const entry of entries) if (entry.responseend > last) last = entry.responseend;
1820
+ return Math.max(0, now - last);
1821
+ }
1822
+ function quietresolution(samples, idle, timeout) {
1823
+ const start = samples[0]?.at ?? 0;
1824
+ const last = samples[samples.length - 1];
1825
+ const waited = Math.max(0, (last?.at ?? 0) - start);
1826
+ const reached = samples.find((sample) => sample.quietfor >= idle);
1827
+ if (reached) return { ok: true, quietfor: reached.quietfor, waited: reached.at - start, samples: samples.length };
1828
+ return { ok: false, quietfor: last?.quietfor ?? 0, waited, samples: samples.length };
1829
+ }
1830
+ function nodehash(summary) {
1831
+ const canonical = [summary.tag, summary.text, ...Object.keys(summary.attributes).sort().map((key) => `${key}=${summary.attributes[key] ?? ""}`)].join("|");
1832
+ let hash = 5381;
1833
+ for (let index = 0; index < canonical.length; index += 1) hash = (hash << 5) + hash + canonical.charCodeAt(index) >>> 0;
1834
+ return hash.toString(16);
1835
+ }
1836
+ function diffsummaries(base, target) {
1837
+ const basemap = new Map(base.map((node) => [node.selector, node]));
1838
+ const targetmap = new Map(target.map((node) => [node.selector, node]));
1839
+ const added = [];
1840
+ const removed = [];
1841
+ const changed = [];
1842
+ for (const [selector, node] of targetmap) {
1843
+ const previous = basemap.get(selector);
1844
+ if (!previous) {
1845
+ added.push({ kind: "added", selector, summary: node.text || node.tag });
1846
+ continue;
1847
+ }
1848
+ if (nodehash(previous) !== nodehash(node)) changed.push({ kind: "changed", selector, summary: `${previous.text || previous.tag} became ${node.text || node.tag}` });
1849
+ }
1850
+ for (const [selector, node] of basemap) {
1851
+ if (!targetmap.has(selector)) removed.push({ kind: "removed", selector, summary: node.text || node.tag });
1852
+ }
1853
+ return { added, removed, changed };
1854
+ }
1855
+ function scanjson(scripts) {
1856
+ const states = [];
1857
+ let refused = 0;
1858
+ for (const script of scripts) {
1859
+ if (script.src) continue;
1860
+ const content = script.content.trim();
1861
+ if (!(script.type.includes("json") || content.startsWith("{") || content.startsWith("["))) continue;
1862
+ try {
1863
+ states.push({ scripturl: script.src, rootpath: script.id, payload: JSON.parse(content) });
1864
+ } catch {
1865
+ refused += 1;
1866
+ }
1867
+ }
1868
+ return { states, refused };
1869
+ }
1870
+ function rankselectors(shape) {
1871
+ const candidates = [];
1872
+ if (shape.id) candidates.push({ selector: `#${shape.id}`, strategy: "id", score: 100 });
1873
+ for (const [name, value] of Object.entries(shape.attributes)) {
1874
+ if (!value) continue;
1875
+ if (name === "name" || name.startsWith("data-") || name.startsWith("aria-")) candidates.push({ selector: `${shape.tag}[${name}="${value}"]`, strategy: "attribute", score: 80 });
1876
+ }
1877
+ if (shape.text) candidates.push({ selector: shape.text, strategy: "text", score: 60 });
1878
+ if (shape.index > 0) candidates.push({ selector: `${shape.tag}:nth-of-type(${shape.index})`, strategy: "structural", score: 40 });
1879
+ return candidates.sort((left, right) => right.score - left.score);
1880
+ }
1881
+ function wait2(ms) {
1882
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
1883
+ }
1884
+ function quietruleof(step) {
1885
+ let options = {};
1886
+ try {
1887
+ options = parseoptions(step);
1888
+ } catch {
1889
+ options = {};
1890
+ }
1891
+ const rule = options.quietrule;
1892
+ if (!rule || typeof rule !== "object" || Array.isArray(rule)) return { idle: 0 };
1893
+ const quiet = rule;
1894
+ return {
1895
+ idle: typeof quiet.idle === "number" && Number.isFinite(quiet.idle) && quiet.idle > 0 ? quiet.idle : 0,
1896
+ ...typeof quiet.poll === "number" && Number.isFinite(quiet.poll) && quiet.poll >= 0 ? { poll: quiet.poll } : {},
1897
+ ...typeof quiet.timeout === "number" && Number.isFinite(quiet.timeout) && quiet.timeout >= 0 ? { timeout: quiet.timeout } : {}
1898
+ };
1899
+ }
1900
+ async function watchmutations(step, root) {
1901
+ const options = parsewatchoptions(step, step.id);
1902
+ if (options.lifetime <= 0) return { ok: false, summary: "The reviewed mutation watch lifetime is absent." };
1903
+ const roots = options.scopes ? options.scopes.flatMap((selector) => [...root.querySelectorAll(selector)]) : [root];
1904
+ if (roots.length === 0) return { ok: false, summary: "The reviewed watch scopes match no elements." };
1905
+ const allowed = options.events;
1906
+ const collected = [];
1907
+ const observer = new MutationObserver((records) => {
1908
+ for (const record of records) {
1909
+ if (allowed && !allowed.includes(record.type)) continue;
1910
+ const target = record.target instanceof Element ? record.target : null;
1911
+ collected.push({ watchid: options.watchid, event: record.type, targetpath: target ? elementselector(target) : "#text", at: Date.now() });
1912
+ }
1913
+ });
1914
+ for (const scope of roots) observer.observe(scope, { childList: true, attributes: true, characterData: true, subtree: true });
1915
+ await wait2(options.lifetime);
1916
+ observer.disconnect();
1917
+ const batches = batchmutations(collected, options.poll);
1918
+ 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 ?? [] } };
1919
+ }
1920
+ async function watchfocus(step, root) {
1921
+ const options = parsewatchoptions(step, step.id);
1922
+ if (options.lifetime <= 0) return { ok: false, summary: "The reviewed focus watch lifetime is absent." };
1923
+ const collected = [];
1924
+ const record = (kind) => (event) => {
1925
+ const target = event.target instanceof Element ? event.target : null;
1926
+ collected.push({ watchid: options.watchid, kind, targetpath: target ? elementselector(target) : "#document", at: Date.now() });
1927
+ };
1928
+ const onfocus = record("focus");
1929
+ const onblur = record("blur");
1930
+ root.addEventListener("focusin", onfocus, true);
1931
+ root.addEventListener("focusout", onblur, true);
1932
+ await wait2(options.lifetime);
1933
+ root.removeEventListener("focusin", onfocus, true);
1934
+ root.removeEventListener("focusout", onblur, true);
1935
+ 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 } };
1936
+ }
1937
+ async function watchbanners(step, root) {
1938
+ const options = parsewatchoptions(step, step.id);
1939
+ if (options.lifetime <= 0) return { ok: false, summary: "The reviewed banner watch lifetime is absent." };
1940
+ const started = Date.now();
1941
+ const seen = /* @__PURE__ */ new Map();
1942
+ while (Date.now() - started < options.lifetime) {
1943
+ const at = Date.now();
1944
+ for (const report of bannermatches(collectbannercandidates(root), at)) {
1945
+ if (!seen.has(report.selector)) seen.set(report.selector, report);
1946
+ }
1947
+ await wait2(options.poll);
1948
+ }
1949
+ const reports = [...seen.values()];
1950
+ 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 } };
1951
+ }
1952
+ async function waitquiet(step) {
1953
+ const rule = quietruleof(step);
1954
+ if (rule.idle <= 0) return { ok: false, summary: "The reviewed quiet idle threshold is absent." };
1955
+ const poll2 = rule.poll ?? 100;
1956
+ const timeout = rule.timeout ?? 0;
1957
+ const started = performance.now();
1958
+ const samples = [];
1959
+ for (; ; ) {
1960
+ const now = performance.now();
1961
+ const entries = performance.getEntriesByType("resource").map((entry) => ({ responseend: entry.responseEnd }));
1962
+ samples.push({ at: now - started, quietfor: quietfor(entries, now) });
1963
+ const latest = samples[samples.length - 1];
1964
+ if (latest && latest.quietfor >= rule.idle) break;
1965
+ if (timeout > 0 && now - started >= timeout) break;
1966
+ await wait2(poll2);
1967
+ }
1968
+ const outcome = quietresolution(samples, rule.idle, timeout);
1969
+ return {
1970
+ ok: outcome.ok,
1971
+ 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` : ""}.`,
1972
+ details: { samples, idle: rule.idle, timeout, waited: Math.round(outcome.waited) }
1973
+ };
1974
+ }
1975
+ function scriptsurfaces(target, root) {
1976
+ const elements = target ? [target] : [...root.querySelectorAll("script")];
1977
+ return elements.map((element) => ({ src: element.getAttribute("src") ?? "", type: element.getAttribute("type") ?? "", id: element.id, content: element.textContent ?? "" }));
1978
+ }
1979
+ function readjson(step, target, root) {
1980
+ const outcome = scanjson(scriptsurfaces(target, root));
1981
+ if (target && outcome.states.length === 0 && outcome.refused > 0) return { ok: false, summary: "The reviewed json payload is malformed and was refused." };
1982
+ 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 } };
1983
+ }
1984
+ function tonodesummaries(value) {
1985
+ if (!Array.isArray(value)) return null;
1986
+ const summaries = [];
1987
+ for (const entry of value) {
1988
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1989
+ const candidate = entry;
1990
+ if (typeof candidate.selector !== "string") continue;
1991
+ const attributes = {};
1992
+ if (candidate.attributes && typeof candidate.attributes === "object" && !Array.isArray(candidate.attributes)) {
1993
+ for (const [key, item] of Object.entries(candidate.attributes)) if (typeof item === "string") attributes[key] = item;
1994
+ }
1995
+ summaries.push({ selector: candidate.selector, tag: typeof candidate.tag === "string" ? candidate.tag : "", text: typeof candidate.text === "string" ? candidate.text : "", attributes });
1996
+ }
1997
+ return summaries;
1998
+ }
1999
+ function diffsnapshots(step) {
2000
+ let options = {};
2001
+ try {
2002
+ options = parseoptions(step);
2003
+ } catch {
2004
+ options = {};
2005
+ }
2006
+ const base = tonodesummaries(options.base);
2007
+ const target = tonodesummaries(options.target);
2008
+ if (!base || !target) return { ok: false, summary: "Two stored observation versions must be reviewed before diffing." };
2009
+ const versions = Array.isArray(options.versions) && options.versions.length === 2 ? options.versions : [0, 0];
2010
+ const diff = diffsummaries(base, target);
2011
+ 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 } };
2012
+ }
2013
+ function deriveselector(target) {
2014
+ if (!(target instanceof Element)) return { ok: false, summary: "Derivation target is no longer available." };
2015
+ const attributes = {};
2016
+ for (const attribute of [...target.attributes]) attributes[attribute.name] = attribute.value;
2017
+ const parent = target.parentElement;
2018
+ const siblings = parent ? [...parent.children].filter((node) => node.tagName === target.tagName) : [target];
2019
+ 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 });
2020
+ const best = candidates[0];
2021
+ 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 } };
2022
+ }
2023
+ function runpagewatch(step, target, root = document) {
2024
+ switch (step.kind) {
2025
+ case "watchmutate":
2026
+ return watchmutations(step, root);
2027
+ case "watchfocus":
2028
+ return watchfocus(step, root);
2029
+ case "watchbanner":
2030
+ return watchbanners(step, root);
2031
+ case "waitquiet":
2032
+ return waitquiet(step);
2033
+ case "readjson":
2034
+ return readjson(step, target, root);
2035
+ case "diffsnapshots":
2036
+ return diffsnapshots(step);
2037
+ case "deriveselector":
2038
+ return deriveselector(target);
2039
+ default:
2040
+ return { ok: false, summary: "Unsupported watched observation." };
2041
+ }
2042
+ }
2043
+
2044
+ // extension/pagebridge.ts
2045
+ function stepoptions2(step) {
1141
2046
  if (!step.options) return {};
1142
2047
  try {
1143
2048
  const parsed = JSON.parse(step.options);
@@ -1177,7 +2082,26 @@
1177
2082
  ...element instanceof HTMLSelectElement ? { options: [...element.options].map((option) => clean(option.textContent || option.value)) } : {}
1178
2083
  }));
1179
2084
  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() };
2085
+ const tree = buildpagetree(document);
2086
+ const tables = collecttables(document).map((entry) => {
2087
+ const shape = normalizetable(entry.rows, entry.caption);
2088
+ return { selector: entry.selector, headers: shape.headers, columns: shape.columns, rows: shape.rows, caption: shape.caption };
2089
+ });
2090
+ return {
2091
+ schemaversion: 3,
2092
+ url: location.href,
2093
+ title: clean(document.title),
2094
+ textpreview: text,
2095
+ textlength: document.body?.innerText.length ?? 0,
2096
+ forms,
2097
+ interactive,
2098
+ capturedat: Date.now(),
2099
+ mode: "passive",
2100
+ a11y: builda11ytree(tree),
2101
+ reader: buildreader(tree, clean(document.title)),
2102
+ listpattern: detectlistpatterns(collectsiblings(document)),
2103
+ tableshape: tables
2104
+ };
1181
2105
  }
1182
2106
  function readdialogs() {
1183
2107
  return harvestdialoglog(document);
@@ -1220,6 +2144,9 @@
1220
2144
  var controlkinds = /* @__PURE__ */ new Set(["typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails"]);
1221
2145
  var interactkinds = /* @__PURE__ */ new Set(["clicktext", "clickaria", "clickname", "pierceshadow", "enterframe"]);
1222
2146
  var pointerkinds = /* @__PURE__ */ new Set(["movepointer", "clickpoint", "shiftclick"]);
2147
+ var observationkinds = /* @__PURE__ */ new Set(["a11ytree", "readvisible", "readertree", "readoutline", "readselection", "readopengraph", "readlang", "detectlanguage", "listshadow", "listframes"]);
2148
+ var detectionkinds = /* @__PURE__ */ new Set(["detectlists", "detecttables", "detectinfinitescroll", "detectvirtual", "detectlazy", "detectsticky", "detectscrolllock", "countpages", "classifypage", "fingerprintsection", "readscrollpos"]);
2149
+ var watchstepkinds = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "waitquiet", "readjson", "diffsnapshots", "deriveselector"]);
1223
2150
  async function performstep(step, expectedorigin, rootdocument = document) {
1224
2151
  if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before action." };
1225
2152
  if (step.kind === "observe") return { ok: true, summary: "Observation completed." };
@@ -1247,7 +2174,7 @@
1247
2174
  return { ok: true, summary: "History forward requested." };
1248
2175
  }
1249
2176
  if (step.kind === "scrollpage") {
1250
- const options = stepoptions(step);
2177
+ const options = stepoptions2(step);
1251
2178
  window.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
1252
2179
  return { ok: true, summary: "Window scrolled by the reviewed amounts." };
1253
2180
  }
@@ -1269,11 +2196,14 @@
1269
2196
  else if (controlkinds.has(step.kind)) result = runpagecontrol(step, element, rootdocument);
1270
2197
  else if (interactkinds.has(step.kind)) return await runinteractstep(step, expectedorigin, performstep);
1271
2198
  else if (pointerkinds.has(step.kind)) result = runpointerstep(step, resolution);
2199
+ else if (observationkinds.has(step.kind)) result = runpageobservation(step, element, rootdocument);
2200
+ else if (detectionkinds.has(step.kind)) result = runpagedetection(step, element, rootdocument);
2201
+ else if (watchstepkinds.has(step.kind)) result = runpagewatch(step, element, rootdocument);
1272
2202
  else if (mutatingkinds.has(step.kind)) result = runpageaction(step, element);
1273
2203
  else {
1274
2204
  if (!element) return { ok: false, summary: "Action target is no longer available." };
1275
2205
  if (step.kind === "scrollby") {
1276
- const options = stepoptions(step);
2206
+ const options = stepoptions2(step);
1277
2207
  element.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
1278
2208
  result = { ok: true, summary: "Container scrolled by the reviewed amounts." };
1279
2209
  } else if (step.kind === "focus") {