@duckduckgo/autoconsent 16.63.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/addon-firefox/background.bundle.js +1 -0
  3. package/dist/addon-firefox/compact-rules.json +1 -1
  4. package/dist/addon-firefox/content.bundle.js +162 -35
  5. package/dist/addon-firefox/manifest.json +1 -1
  6. package/dist/addon-firefox/popup.bundle.js +1 -0
  7. package/dist/addon-firefox/rule-index.json +1 -1
  8. package/dist/addon-firefox/rules.json +1 -1
  9. package/dist/addon-mv3/background.bundle.js +1 -0
  10. package/dist/addon-mv3/compact-rules.json +1 -1
  11. package/dist/addon-mv3/content.bundle.js +162 -35
  12. package/dist/addon-mv3/manifest.json +1 -1
  13. package/dist/addon-mv3/popup.bundle.js +1 -0
  14. package/dist/addon-mv3/rule-index.json +1 -1
  15. package/dist/addon-mv3/rules.json +1 -1
  16. package/dist/autoconsent.cjs.js +162 -35
  17. package/dist/autoconsent.esm.js +162 -35
  18. package/dist/autoconsent.playwright.js +165 -35
  19. package/dist/autoconsent.standalone.js +163 -36
  20. package/dist/types/heuristics.d.ts +12 -3
  21. package/dist/types/types.d.ts +12 -1
  22. package/lib/cmps/base.ts +5 -1
  23. package/lib/heuristics.ts +240 -45
  24. package/lib/types.ts +16 -1
  25. package/lib/utils.ts +1 -0
  26. package/package.json +1 -1
  27. package/readme.md +1 -1
  28. package/rules/autoconsent/asrock.json +44 -0
  29. package/rules/autoconsent/consentmo.json +5 -2
  30. package/rules/autoconsent/cookie-script.json +34 -15
  31. package/rules/autoconsent/epaas.json +18 -1
  32. package/rules/autoconsent/fnsp-cookieconsent.json +30 -0
  33. package/rules/autoconsent/fod-fujitv.json +13 -0
  34. package/rules/autoconsent/hkdir.json +33 -0
  35. package/rules/autoconsent/jokul-cookie-consent.json +28 -0
  36. package/rules/autoconsent/lego.json +27 -5
  37. package/rules/autoconsent/mars-cookies-prompt.json +20 -0
  38. package/rules/autoconsent/niedziela-pl.json +35 -0
  39. package/rules/autoconsent/nikkeibp.json +10 -0
  40. package/rules/autoconsent/onecom-cookie-banner.json +25 -0
  41. package/rules/autoconsent/oslosportslager.no.json +34 -0
  42. package/rules/autoconsent/plex.tv.json +52 -0
  43. package/rules/autoconsent/radio-canada.json +28 -0
  44. package/rules/autoconsent/regjeringen.no.json +13 -0
  45. package/rules/autoconsent/roll20.json +50 -0
  46. package/rules/autoconsent/sirdata.json +8 -3
  47. package/rules/autoconsent/site-notifications-cookie-usage.json +25 -0
  48. package/rules/autoconsent/sparebank1-optin.json +10 -0
  49. package/rules/autoconsent/tri-table.json +6 -2
  50. package/rules/autoconsent/vinnova-se.json +50 -0
  51. package/rules/autoconsent/vortex-consent.json +33 -0
  52. package/rules/compact-rules.json +1 -1
  53. package/rules/rule-index.json +1 -1
  54. package/rules/rules.json +1 -1
  55. package/tests/asrock.spec.ts +3 -0
  56. package/tests/consentmo.spec.ts +2 -0
  57. package/tests/cookie-script.spec.ts +5 -0
  58. package/tests/epaas.spec.ts +3 -0
  59. package/tests/fnsp-cookieconsent.spec.ts +9 -0
  60. package/tests/fod-fujitv.spec.ts +3 -0
  61. package/tests/hkdir.spec.ts +10 -0
  62. package/tests/jokul-cookie-consent.spec.ts +3 -0
  63. package/tests/mars-cookies-prompt.spec.ts +3 -0
  64. package/tests/niedziela-pl.spec.ts +3 -0
  65. package/tests/nikkeibp.spec.ts +8 -0
  66. package/tests/onecom-cookie-banner.spec.ts +5 -0
  67. package/tests/oslosportslager.no.spec.ts +3 -0
  68. package/tests/plex.tv.spec.ts +5 -0
  69. package/tests/radio-canada.spec.ts +15 -0
  70. package/tests/regjeringen.no.spec.ts +3 -0
  71. package/tests/roll20.spec.ts +3 -0
  72. package/tests/sirdata.spec.ts +13 -1
  73. package/tests/site-notifications-cookie-usage.spec.ts +3 -0
  74. package/tests/sparebank1-optin.spec.ts +8 -0
  75. package/tests/tri-table.spec.ts +1 -1
  76. package/tests/vinnova-se.spec.ts +6 -0
  77. package/tests/vortex-consent.spec.ts +18 -0
  78. package/tests-wtr/heuristics/get-actionable-popups.ts +41 -7
  79. package/tests-wtr/heuristics/get-potential-popups.test.ts +235 -4
  80. package/tests-wtr/heuristics/heuristic-cmp.ts +16 -1
  81. package/rules/generated/auto_CA_plex.tv_ee4.json +0 -40
  82. package/rules/generated/auto_CA_support.plex.tv_gn3.json +0 -40
  83. package/tests/generated/auto_CA_plex.tv_ee4.spec.ts +0 -2
  84. package/tests/generated/auto_CA_support.plex.tv_gn3.spec.ts +0 -6
@@ -187,6 +187,7 @@ function normalizeConfig(providedConfig) {
187
187
  },
188
188
  performanceLoggingEnabled: false,
189
189
  heuristicPopupSearchTimeout: 100,
190
+ enableHeuristicHitTest: false,
190
191
  heuristicMode: "off"
191
192
  // heuristic disabled by default
192
193
  };
@@ -1362,28 +1363,70 @@ function classifyButtonTextRegex(buttonText) {
1362
1363
  // lib/heuristics.ts
1363
1364
  var BUTTON_LIKE_ELEMENT_SELECTOR = 'button, input[type="button"], input[type="submit"], a, [role="button"], [class*="button"]';
1364
1365
  var POPUP_SEARCH_MAX_TIME = 100;
1365
- function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME) {
1366
- const acceptedLevels = mode === "tier2" ? ["reject", "tier1", "tier2"] : mode === "tier1" ? ["reject", "tier1"] : ["reject"];
1367
- const popups = getPotentialPopups(timeout).filter((popup) => {
1368
- const popupText = popup.text?.trim();
1369
- if (!popupText || isExcludedPopup(popupText) || !hasHeuristicPatterns(popupText)) {
1370
- return false;
1371
- }
1372
- popup.regexClassification = classifyPopup(popup.buttons);
1373
- return popup.regexClassification !== void 0 && acceptedLevels.includes(popup.regexClassification);
1374
- });
1375
- return popups.sort((a, b) => (a.regexClassification ?? "") > (b.regexClassification ?? "") ? 1 : -1);
1366
+ var ACCEPTED_LEVELS = {
1367
+ off: [],
1368
+ reject: ["reject"],
1369
+ tier1: ["reject", "tier1"],
1370
+ tier2: ["reject", "tier1", "tier2"]
1371
+ };
1372
+ function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME, hitTest = false) {
1373
+ const acceptedLevels = ACCEPTED_LEVELS[mode];
1374
+ const isActionable = (popup) => acceptedLevels.includes(popup.regexClassification) && isConsentPopup(popup);
1375
+ const popups = searchPopups(timeout, hitTest, DEFAULT_HIT_TEST_GRID, true).filter(isActionable);
1376
+ return popups.sort((a, b) => classificationRank(a) - classificationRank(b));
1376
1377
  }
1377
- function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1378
+ function searchPopups(timeout, hitTest, hitTestGrid, stopOnReject = false) {
1378
1379
  if (!isTopFrame()) {
1379
- if (window.parent && window.parent !== window.top) {
1380
- return [];
1381
- }
1382
- const frameElement = getFramePopupElement(document);
1383
- const popup = frameElement && toPopupData(frameElement);
1384
- return popup ? [popup] : [];
1380
+ return getFramePopups();
1385
1381
  }
1386
1382
  const deadline = performance.now() + timeout;
1383
+ const hitTestPopups = hitTest ? findPopupsByHitTest(deadline, hitTestGrid) : [];
1384
+ if (stopOnReject && hitTestPopups.some((p) => p.regexClassification === "reject" && isConsentPopup(p))) {
1385
+ return hitTestPopups;
1386
+ }
1387
+ const walkPopups = collectPositionedPopups(deadline);
1388
+ return [...walkPopups, ...mergeWalkOverlaps(hitTestPopups, walkPopups)];
1389
+ }
1390
+ var consentPopups = /* @__PURE__ */ new WeakMap();
1391
+ function classificationRank(popup) {
1392
+ return ACCEPTED_LEVELS.tier2.indexOf(popup.regexClassification);
1393
+ }
1394
+ function isConsentPopup(popup) {
1395
+ if (!ACCEPTED_LEVELS.tier2.includes(popup.regexClassification)) {
1396
+ return false;
1397
+ }
1398
+ let result = consentPopups.get(popup);
1399
+ if (result === void 0) {
1400
+ const popupText = popup.text.trim();
1401
+ result = !!popupText && !isExcludedPopup(popupText) && hasHeuristicPatterns(popupText);
1402
+ consentPopups.set(popup, result);
1403
+ }
1404
+ return result;
1405
+ }
1406
+ function mergeWalkOverlaps(hitTestPopups, walkPopups) {
1407
+ return hitTestPopups.filter((h) => {
1408
+ const hitIsConsent = isConsentPopup(h);
1409
+ const absorbs = (p) => !hitIsConsent || isConsentPopup(p) && classificationRank(p) <= classificationRank(h);
1410
+ const walkPopup = walkPopups.find((p) => composedOverlaps(p.element, h.element) && absorbs(p));
1411
+ if (walkPopup) {
1412
+ (walkPopup.hitPoints ??= []).push(...h.hitPoints ?? []);
1413
+ }
1414
+ return !walkPopup;
1415
+ });
1416
+ }
1417
+ function getFramePopups() {
1418
+ if (window.parent && window.parent !== window.top) {
1419
+ return [];
1420
+ }
1421
+ const frameElement = getFramePopupElement(document);
1422
+ const popup = frameElement && toPopupData(frameElement);
1423
+ if (!popup) {
1424
+ return [];
1425
+ }
1426
+ popup.source = "frame";
1427
+ return [popup];
1428
+ }
1429
+ function collectPositionedPopups(deadline) {
1387
1430
  const { elements, shadowHosts } = getPopupLikeElements(deadline);
1388
1431
  const potentialPopups = [];
1389
1432
  const expanded = [];
@@ -1399,17 +1442,86 @@ function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1399
1442
  if (root !== popup) {
1400
1443
  expanded.push(root.element);
1401
1444
  }
1445
+ root.source = "walk";
1402
1446
  potentialPopups.push(root);
1403
1447
  }
1404
1448
  return potentialPopups.filter((p) => !expanded.some((root) => root !== p.element && composedContains(root, p.element)));
1405
1449
  }
1450
+ var DEFAULT_HIT_TEST_GRID = {
1451
+ columns: [0.1, 0.5, 0.9],
1452
+ // banners sit along the top and bottom edges and modals in the middle; rows 150px in added almost nothing in crawls
1453
+ edgeRows: [30, 90]
1454
+ };
1455
+ function findPopupsByHitTest(deadline, grid) {
1456
+ const climbed = /* @__PURE__ */ new Map();
1457
+ const found = [];
1458
+ const toPopup = (el) => el.querySelector(TEXT_ENTRY_SELECTOR) ? null : toPopupRoot(el);
1459
+ for (const [x, y] of getHitTestPoints(grid)) {
1460
+ if (performance.now() > deadline) {
1461
+ return found;
1462
+ }
1463
+ const path = [];
1464
+ let reached = null;
1465
+ const shouldStop = (el) => {
1466
+ if (climbed.has(el)) {
1467
+ reached = climbed.get(el) ?? null;
1468
+ return true;
1469
+ }
1470
+ const overlapped = found.find((p) => composedOverlaps(p.element, el));
1471
+ if (overlapped) {
1472
+ reached = composedContains(overlapped.element, el) ? overlapped : null;
1473
+ return true;
1474
+ }
1475
+ path.push(el);
1476
+ return false;
1477
+ };
1478
+ const hit = closestHTMLElement(deepElementFromPoint(x, y));
1479
+ const popup = climbToPopup(hit, MAX_HIT_TEST_CLIMB_DEPTH, toPopup, shouldStop);
1480
+ if (popup) {
1481
+ popup.source = "hitTest";
1482
+ found.push(popup);
1483
+ reached = popup;
1484
+ }
1485
+ for (const el of path) {
1486
+ climbed.set(el, reached);
1487
+ }
1488
+ if (reached) {
1489
+ (reached.hitPoints ??= []).push([x, y]);
1490
+ }
1491
+ }
1492
+ return found;
1493
+ }
1494
+ function getHitTestPoints(grid) {
1495
+ const { innerWidth: width, innerHeight: height } = window;
1496
+ const rows = [...grid.edgeRows, height / 2, ...grid.edgeRows.map((y) => height - y)].map((y) => Math.round(y));
1497
+ const ys = [...new Set(rows)].filter((y) => y >= 0 && y < height);
1498
+ return ys.flatMap((y) => grid.columns.map((column) => [Math.round(column * width), y]));
1499
+ }
1500
+ var TEXT_ENTRY_SELECTOR = 'textarea, input:not([type]), input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="tel"], input[type="number"], input[type="url"]';
1501
+ function deepElementFromPoint(x, y) {
1502
+ let el = document.elementFromPoint(x, y);
1503
+ while (el?.shadowRoot) {
1504
+ const inner = el.shadowRoot.elementFromPoint(x, y);
1505
+ if (!inner || inner === el) {
1506
+ break;
1507
+ }
1508
+ el = inner;
1509
+ }
1510
+ return el;
1511
+ }
1512
+ function closestHTMLElement(el) {
1513
+ while (el && !(el instanceof HTMLElement)) {
1514
+ el = el.parentElement;
1515
+ }
1516
+ return el;
1517
+ }
1406
1518
  function toPopupData(el, shadowHosts, text = getElementText(el)) {
1407
1519
  if (!text) {
1408
1520
  return null;
1409
1521
  }
1410
1522
  const buttons = getButtonData(el, shadowHosts);
1411
1523
  classifyButtons(buttons);
1412
- return { text, element: el, buttons };
1524
+ return { text, element: el, buttons, regexClassification: classifyPopup(buttons) };
1413
1525
  }
1414
1526
  function hasPopupButtons(text, buttons) {
1415
1527
  return !isOnlyButtonText(text, buttons) && buttons.some((b) => b.regexClassification !== "other");
@@ -1421,26 +1533,34 @@ function isOnlyButtonText(text, buttons) {
1421
1533
  }
1422
1534
  return rest.replace(/\s+/g, "").length < 10;
1423
1535
  }
1536
+ function toPopupRoot(el, shadowHosts) {
1537
+ const text = getElementText(el);
1538
+ if (!text || !hasHeuristicPatterns(text)) {
1539
+ return null;
1540
+ }
1541
+ const popup = toPopupData(el, shadowHosts, text);
1542
+ return popup && hasPopupButtons(text, popup.buttons) ? popup : null;
1543
+ }
1424
1544
  var MAX_FRAGMENT_EXPANSION_DEPTH = 6;
1545
+ var MAX_HIT_TEST_CLIMB_DEPTH = 8;
1546
+ function climbToPopup(el, maxDepth, toPopup, shouldStop) {
1547
+ for (let depth = 0; el && depth < maxDepth; depth++, el = getComposedParent(el)) {
1548
+ if (shouldStop?.(el) || el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1549
+ return null;
1550
+ }
1551
+ const popup = toPopup(el);
1552
+ if (popup) {
1553
+ return popup;
1554
+ }
1555
+ }
1556
+ return null;
1557
+ }
1425
1558
  function expandPopupFragment(popup, shadowHosts) {
1426
1559
  if (hasHeuristicPatterns(popup.text) && hasPopupButtons(popup.text, popup.buttons)) {
1427
1560
  return popup;
1428
1561
  }
1429
- let el = getComposedParent(popup.element);
1430
- for (let depth = 0; el && depth < MAX_FRAGMENT_EXPANSION_DEPTH; depth++, el = getComposedParent(el)) {
1431
- if (el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1432
- break;
1433
- }
1434
- const text = getElementText(el);
1435
- if (!text || !hasHeuristicPatterns(text)) {
1436
- continue;
1437
- }
1438
- const root = toPopupData(el, shadowHosts, text);
1439
- if (root && hasPopupButtons(text, root.buttons)) {
1440
- return root;
1441
- }
1442
- }
1443
- return popup;
1562
+ const parent = getComposedParent(popup.element);
1563
+ return climbToPopup(parent, MAX_FRAGMENT_EXPANSION_DEPTH, (el) => toPopupRoot(el, shadowHosts)) ?? popup;
1444
1564
  }
1445
1565
  function getComposedParent(el) {
1446
1566
  if (el.parentElement) {
@@ -1471,6 +1591,9 @@ function getElementText(el) {
1471
1591
  function isRenderedTextElement(el) {
1472
1592
  return el instanceof HTMLElement && !["STYLE", "SCRIPT", "TEMPLATE", "NOSCRIPT", "LINK", "SLOT"].includes(el.tagName) && window.getComputedStyle(el).display !== "none";
1473
1593
  }
1594
+ function composedOverlaps(a, b) {
1595
+ return composedContains(a, b) || composedContains(b, a);
1596
+ }
1474
1597
  function composedContains(container, node) {
1475
1598
  if (container.contains(node)) {
1476
1599
  return true;
@@ -1962,7 +2085,11 @@ var AutoConsentHeuristicCMP = class extends AutoConsentCMPBase {
1962
2085
  async detectCmp() {
1963
2086
  await new Promise((resolve) => setTimeout(resolve, 0));
1964
2087
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorStart");
1965
- this.popups = getActionablePopups(this.mode, this.autoconsent.config.heuristicPopupSearchTimeout);
2088
+ this.popups = getActionablePopups(
2089
+ this.mode,
2090
+ this.autoconsent.config.heuristicPopupSearchTimeout,
2091
+ this.autoconsent.config.enableHeuristicHitTest
2092
+ );
1966
2093
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorEnd");
1967
2094
  this.autoconsent.config.performanceLoggingEnabled && performance.measure("heuristicDetector", "heuristicDetectorStart", "heuristicDetectorEnd");
1968
2095
  if (this.popups.length > 0) {
@@ -157,6 +157,7 @@ function normalizeConfig(providedConfig) {
157
157
  },
158
158
  performanceLoggingEnabled: false,
159
159
  heuristicPopupSearchTimeout: 100,
160
+ enableHeuristicHitTest: false,
160
161
  heuristicMode: "off"
161
162
  // heuristic disabled by default
162
163
  };
@@ -1332,28 +1333,70 @@ function classifyButtonTextRegex(buttonText) {
1332
1333
  // lib/heuristics.ts
1333
1334
  var BUTTON_LIKE_ELEMENT_SELECTOR = 'button, input[type="button"], input[type="submit"], a, [role="button"], [class*="button"]';
1334
1335
  var POPUP_SEARCH_MAX_TIME = 100;
1335
- function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME) {
1336
- const acceptedLevels = mode === "tier2" ? ["reject", "tier1", "tier2"] : mode === "tier1" ? ["reject", "tier1"] : ["reject"];
1337
- const popups = getPotentialPopups(timeout).filter((popup) => {
1338
- const popupText = popup.text?.trim();
1339
- if (!popupText || isExcludedPopup(popupText) || !hasHeuristicPatterns(popupText)) {
1340
- return false;
1341
- }
1342
- popup.regexClassification = classifyPopup(popup.buttons);
1343
- return popup.regexClassification !== void 0 && acceptedLevels.includes(popup.regexClassification);
1344
- });
1345
- return popups.sort((a, b) => (a.regexClassification ?? "") > (b.regexClassification ?? "") ? 1 : -1);
1336
+ var ACCEPTED_LEVELS = {
1337
+ off: [],
1338
+ reject: ["reject"],
1339
+ tier1: ["reject", "tier1"],
1340
+ tier2: ["reject", "tier1", "tier2"]
1341
+ };
1342
+ function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME, hitTest = false) {
1343
+ const acceptedLevels = ACCEPTED_LEVELS[mode];
1344
+ const isActionable = (popup) => acceptedLevels.includes(popup.regexClassification) && isConsentPopup(popup);
1345
+ const popups = searchPopups(timeout, hitTest, DEFAULT_HIT_TEST_GRID, true).filter(isActionable);
1346
+ return popups.sort((a, b) => classificationRank(a) - classificationRank(b));
1346
1347
  }
1347
- function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1348
+ function searchPopups(timeout, hitTest, hitTestGrid, stopOnReject = false) {
1348
1349
  if (!isTopFrame()) {
1349
- if (window.parent && window.parent !== window.top) {
1350
- return [];
1351
- }
1352
- const frameElement = getFramePopupElement(document);
1353
- const popup = frameElement && toPopupData(frameElement);
1354
- return popup ? [popup] : [];
1350
+ return getFramePopups();
1355
1351
  }
1356
1352
  const deadline = performance.now() + timeout;
1353
+ const hitTestPopups = hitTest ? findPopupsByHitTest(deadline, hitTestGrid) : [];
1354
+ if (stopOnReject && hitTestPopups.some((p) => p.regexClassification === "reject" && isConsentPopup(p))) {
1355
+ return hitTestPopups;
1356
+ }
1357
+ const walkPopups = collectPositionedPopups(deadline);
1358
+ return [...walkPopups, ...mergeWalkOverlaps(hitTestPopups, walkPopups)];
1359
+ }
1360
+ var consentPopups = /* @__PURE__ */ new WeakMap();
1361
+ function classificationRank(popup) {
1362
+ return ACCEPTED_LEVELS.tier2.indexOf(popup.regexClassification);
1363
+ }
1364
+ function isConsentPopup(popup) {
1365
+ if (!ACCEPTED_LEVELS.tier2.includes(popup.regexClassification)) {
1366
+ return false;
1367
+ }
1368
+ let result = consentPopups.get(popup);
1369
+ if (result === void 0) {
1370
+ const popupText = popup.text.trim();
1371
+ result = !!popupText && !isExcludedPopup(popupText) && hasHeuristicPatterns(popupText);
1372
+ consentPopups.set(popup, result);
1373
+ }
1374
+ return result;
1375
+ }
1376
+ function mergeWalkOverlaps(hitTestPopups, walkPopups) {
1377
+ return hitTestPopups.filter((h) => {
1378
+ const hitIsConsent = isConsentPopup(h);
1379
+ const absorbs = (p) => !hitIsConsent || isConsentPopup(p) && classificationRank(p) <= classificationRank(h);
1380
+ const walkPopup = walkPopups.find((p) => composedOverlaps(p.element, h.element) && absorbs(p));
1381
+ if (walkPopup) {
1382
+ (walkPopup.hitPoints ??= []).push(...h.hitPoints ?? []);
1383
+ }
1384
+ return !walkPopup;
1385
+ });
1386
+ }
1387
+ function getFramePopups() {
1388
+ if (window.parent && window.parent !== window.top) {
1389
+ return [];
1390
+ }
1391
+ const frameElement = getFramePopupElement(document);
1392
+ const popup = frameElement && toPopupData(frameElement);
1393
+ if (!popup) {
1394
+ return [];
1395
+ }
1396
+ popup.source = "frame";
1397
+ return [popup];
1398
+ }
1399
+ function collectPositionedPopups(deadline) {
1357
1400
  const { elements, shadowHosts } = getPopupLikeElements(deadline);
1358
1401
  const potentialPopups = [];
1359
1402
  const expanded = [];
@@ -1369,17 +1412,86 @@ function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1369
1412
  if (root !== popup) {
1370
1413
  expanded.push(root.element);
1371
1414
  }
1415
+ root.source = "walk";
1372
1416
  potentialPopups.push(root);
1373
1417
  }
1374
1418
  return potentialPopups.filter((p) => !expanded.some((root) => root !== p.element && composedContains(root, p.element)));
1375
1419
  }
1420
+ var DEFAULT_HIT_TEST_GRID = {
1421
+ columns: [0.1, 0.5, 0.9],
1422
+ // banners sit along the top and bottom edges and modals in the middle; rows 150px in added almost nothing in crawls
1423
+ edgeRows: [30, 90]
1424
+ };
1425
+ function findPopupsByHitTest(deadline, grid) {
1426
+ const climbed = /* @__PURE__ */ new Map();
1427
+ const found = [];
1428
+ const toPopup = (el) => el.querySelector(TEXT_ENTRY_SELECTOR) ? null : toPopupRoot(el);
1429
+ for (const [x, y] of getHitTestPoints(grid)) {
1430
+ if (performance.now() > deadline) {
1431
+ return found;
1432
+ }
1433
+ const path = [];
1434
+ let reached = null;
1435
+ const shouldStop = (el) => {
1436
+ if (climbed.has(el)) {
1437
+ reached = climbed.get(el) ?? null;
1438
+ return true;
1439
+ }
1440
+ const overlapped = found.find((p) => composedOverlaps(p.element, el));
1441
+ if (overlapped) {
1442
+ reached = composedContains(overlapped.element, el) ? overlapped : null;
1443
+ return true;
1444
+ }
1445
+ path.push(el);
1446
+ return false;
1447
+ };
1448
+ const hit = closestHTMLElement(deepElementFromPoint(x, y));
1449
+ const popup = climbToPopup(hit, MAX_HIT_TEST_CLIMB_DEPTH, toPopup, shouldStop);
1450
+ if (popup) {
1451
+ popup.source = "hitTest";
1452
+ found.push(popup);
1453
+ reached = popup;
1454
+ }
1455
+ for (const el of path) {
1456
+ climbed.set(el, reached);
1457
+ }
1458
+ if (reached) {
1459
+ (reached.hitPoints ??= []).push([x, y]);
1460
+ }
1461
+ }
1462
+ return found;
1463
+ }
1464
+ function getHitTestPoints(grid) {
1465
+ const { innerWidth: width, innerHeight: height } = window;
1466
+ const rows = [...grid.edgeRows, height / 2, ...grid.edgeRows.map((y) => height - y)].map((y) => Math.round(y));
1467
+ const ys = [...new Set(rows)].filter((y) => y >= 0 && y < height);
1468
+ return ys.flatMap((y) => grid.columns.map((column) => [Math.round(column * width), y]));
1469
+ }
1470
+ var TEXT_ENTRY_SELECTOR = 'textarea, input:not([type]), input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="tel"], input[type="number"], input[type="url"]';
1471
+ function deepElementFromPoint(x, y) {
1472
+ let el = document.elementFromPoint(x, y);
1473
+ while (el?.shadowRoot) {
1474
+ const inner = el.shadowRoot.elementFromPoint(x, y);
1475
+ if (!inner || inner === el) {
1476
+ break;
1477
+ }
1478
+ el = inner;
1479
+ }
1480
+ return el;
1481
+ }
1482
+ function closestHTMLElement(el) {
1483
+ while (el && !(el instanceof HTMLElement)) {
1484
+ el = el.parentElement;
1485
+ }
1486
+ return el;
1487
+ }
1376
1488
  function toPopupData(el, shadowHosts, text = getElementText(el)) {
1377
1489
  if (!text) {
1378
1490
  return null;
1379
1491
  }
1380
1492
  const buttons = getButtonData(el, shadowHosts);
1381
1493
  classifyButtons(buttons);
1382
- return { text, element: el, buttons };
1494
+ return { text, element: el, buttons, regexClassification: classifyPopup(buttons) };
1383
1495
  }
1384
1496
  function hasPopupButtons(text, buttons) {
1385
1497
  return !isOnlyButtonText(text, buttons) && buttons.some((b) => b.regexClassification !== "other");
@@ -1391,26 +1503,34 @@ function isOnlyButtonText(text, buttons) {
1391
1503
  }
1392
1504
  return rest.replace(/\s+/g, "").length < 10;
1393
1505
  }
1506
+ function toPopupRoot(el, shadowHosts) {
1507
+ const text = getElementText(el);
1508
+ if (!text || !hasHeuristicPatterns(text)) {
1509
+ return null;
1510
+ }
1511
+ const popup = toPopupData(el, shadowHosts, text);
1512
+ return popup && hasPopupButtons(text, popup.buttons) ? popup : null;
1513
+ }
1394
1514
  var MAX_FRAGMENT_EXPANSION_DEPTH = 6;
1515
+ var MAX_HIT_TEST_CLIMB_DEPTH = 8;
1516
+ function climbToPopup(el, maxDepth, toPopup, shouldStop) {
1517
+ for (let depth = 0; el && depth < maxDepth; depth++, el = getComposedParent(el)) {
1518
+ if (shouldStop?.(el) || el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1519
+ return null;
1520
+ }
1521
+ const popup = toPopup(el);
1522
+ if (popup) {
1523
+ return popup;
1524
+ }
1525
+ }
1526
+ return null;
1527
+ }
1395
1528
  function expandPopupFragment(popup, shadowHosts) {
1396
1529
  if (hasHeuristicPatterns(popup.text) && hasPopupButtons(popup.text, popup.buttons)) {
1397
1530
  return popup;
1398
1531
  }
1399
- let el = getComposedParent(popup.element);
1400
- for (let depth = 0; el && depth < MAX_FRAGMENT_EXPANSION_DEPTH; depth++, el = getComposedParent(el)) {
1401
- if (el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1402
- break;
1403
- }
1404
- const text = getElementText(el);
1405
- if (!text || !hasHeuristicPatterns(text)) {
1406
- continue;
1407
- }
1408
- const root = toPopupData(el, shadowHosts, text);
1409
- if (root && hasPopupButtons(text, root.buttons)) {
1410
- return root;
1411
- }
1412
- }
1413
- return popup;
1532
+ const parent = getComposedParent(popup.element);
1533
+ return climbToPopup(parent, MAX_FRAGMENT_EXPANSION_DEPTH, (el) => toPopupRoot(el, shadowHosts)) ?? popup;
1414
1534
  }
1415
1535
  function getComposedParent(el) {
1416
1536
  if (el.parentElement) {
@@ -1441,6 +1561,9 @@ function getElementText(el) {
1441
1561
  function isRenderedTextElement(el) {
1442
1562
  return el instanceof HTMLElement && !["STYLE", "SCRIPT", "TEMPLATE", "NOSCRIPT", "LINK", "SLOT"].includes(el.tagName) && window.getComputedStyle(el).display !== "none";
1443
1563
  }
1564
+ function composedOverlaps(a, b) {
1565
+ return composedContains(a, b) || composedContains(b, a);
1566
+ }
1444
1567
  function composedContains(container, node) {
1445
1568
  if (container.contains(node)) {
1446
1569
  return true;
@@ -1932,7 +2055,11 @@ var AutoConsentHeuristicCMP = class extends AutoConsentCMPBase {
1932
2055
  async detectCmp() {
1933
2056
  await new Promise((resolve) => setTimeout(resolve, 0));
1934
2057
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorStart");
1935
- this.popups = getActionablePopups(this.mode, this.autoconsent.config.heuristicPopupSearchTimeout);
2058
+ this.popups = getActionablePopups(
2059
+ this.mode,
2060
+ this.autoconsent.config.heuristicPopupSearchTimeout,
2061
+ this.autoconsent.config.enableHeuristicHitTest
2062
+ );
1936
2063
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorEnd");
1937
2064
  this.autoconsent.config.performanceLoggingEnabled && performance.measure("heuristicDetector", "heuristicDetectorStart", "heuristicDetectorEnd");
1938
2065
  if (this.popups.length > 0) {