@duckduckgo/autoconsent 16.62.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 (139) hide show
  1. package/CHANGELOG.md +84 -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 +176 -36
  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 +176 -36
  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 +176 -36
  17. package/dist/autoconsent.esm.js +176 -36
  18. package/dist/autoconsent.playwright.js +179 -36
  19. package/dist/autoconsent.standalone.js +177 -37
  20. package/dist/heuristics.cjs.js +14 -1
  21. package/dist/heuristics.mjs +14 -1
  22. package/dist/types/heuristics.d.ts +12 -3
  23. package/dist/types/types.d.ts +12 -1
  24. package/lib/cmps/base.ts +5 -1
  25. package/lib/heuristic-patterns.ts +17 -1
  26. package/lib/heuristics.ts +240 -45
  27. package/lib/types.ts +16 -1
  28. package/lib/utils.ts +1 -0
  29. package/package.json +2 -2
  30. package/readme.md +1 -1
  31. package/rules/autoconsent/appconsent-legacy.json +22 -1
  32. package/rules/autoconsent/asrock.json +44 -0
  33. package/rules/autoconsent/beetle-tracking.json +22 -0
  34. package/rules/autoconsent/casier-judiciaire-justice-gouv-fr.json +40 -0
  35. package/rules/autoconsent/civilservicejobs-service-gov-uk.json +38 -0
  36. package/rules/autoconsent/ckies.json +1 -1
  37. package/rules/autoconsent/consentmo.json +5 -2
  38. package/rules/autoconsent/cookie-script.json +34 -15
  39. package/rules/autoconsent/cookiesinfo-closecookies.json +9 -0
  40. package/rules/autoconsent/dack365.json +35 -0
  41. package/rules/autoconsent/epaas.json +18 -1
  42. package/rules/autoconsent/eu-cookie-law-popup.json +33 -0
  43. package/rules/autoconsent/fascicolosanitario-lombardia.json +14 -0
  44. package/rules/autoconsent/fnsp-cookieconsent.json +30 -0
  45. package/rules/autoconsent/fod-fujitv.json +13 -0
  46. package/rules/autoconsent/fundingchoices.json +12 -2
  47. package/rules/autoconsent/hkdir.json +33 -0
  48. package/rules/autoconsent/hyrabostad.json +21 -0
  49. package/rules/autoconsent/insided.json +15 -0
  50. package/rules/autoconsent/jokul-cookie-consent.json +28 -0
  51. package/rules/autoconsent/kartverket.json +46 -0
  52. package/rules/autoconsent/lego.json +27 -5
  53. package/rules/autoconsent/luxmed.json +33 -0
  54. package/rules/autoconsent/mars-cookies-prompt.json +20 -0
  55. package/rules/autoconsent/meta-cookie-dialog.json +37 -0
  56. package/rules/autoconsent/mittanbud.json +33 -0
  57. package/rules/autoconsent/niedziela-pl.json +35 -0
  58. package/rules/autoconsent/nikkeibp.json +10 -0
  59. package/rules/autoconsent/onecom-cookie-banner.json +25 -0
  60. package/rules/autoconsent/oslosportslager.no.json +34 -0
  61. package/rules/autoconsent/plex.tv.json +52 -0
  62. package/rules/autoconsent/poki.json +37 -0
  63. package/rules/autoconsent/polisen-se.json +26 -0
  64. package/rules/autoconsent/radio-canada.json +28 -0
  65. package/rules/autoconsent/regjeringen.no.json +13 -0
  66. package/rules/autoconsent/roll20.json +50 -0
  67. package/rules/autoconsent/romeo.json +47 -0
  68. package/rules/autoconsent/sirdata.json +8 -3
  69. package/rules/autoconsent/site-notifications-cookie-usage.json +25 -0
  70. package/rules/autoconsent/sparebank1-optin.json +10 -0
  71. package/rules/autoconsent/stockholm-stad.json +42 -0
  72. package/rules/autoconsent/stright.json +19 -1
  73. package/rules/autoconsent/stripchat.com.json +26 -5
  74. package/rules/autoconsent/tri-table.json +6 -2
  75. package/rules/autoconsent/tvp.json +27 -0
  76. package/rules/autoconsent/urvaerket.json +33 -0
  77. package/rules/autoconsent/vinnova-se.json +50 -0
  78. package/rules/autoconsent/volvocars-com.json +23 -2
  79. package/rules/autoconsent/vortex-consent.json +33 -0
  80. package/rules/autoconsent/vue-cookie-accept-decline.json +42 -0
  81. package/rules/compact-rules.json +1 -1
  82. package/rules/rule-index.json +1 -1
  83. package/rules/rules.json +1 -1
  84. package/tests/appconsent-legacy.spec.ts +1 -1
  85. package/tests/asrock.spec.ts +3 -0
  86. package/tests/beetle-tracking.spec.ts +3 -0
  87. package/tests/casier-judiciaire-justice-gouv-fr.spec.ts +3 -0
  88. package/tests/civilservicejobs-service-gov-uk.spec.ts +3 -0
  89. package/tests/consentmo.spec.ts +2 -0
  90. package/tests/cookie-script.spec.ts +5 -0
  91. package/tests/cookiesinfo-closecookies.spec.ts +8 -0
  92. package/tests/dack365.spec.ts +3 -0
  93. package/tests/epaas.spec.ts +3 -0
  94. package/tests/eu-cookie-law-popup.spec.ts +8 -0
  95. package/tests/fascicolosanitario-lombardia.spec.ts +3 -0
  96. package/tests/fnsp-cookieconsent.spec.ts +9 -0
  97. package/tests/fod-fujitv.spec.ts +3 -0
  98. package/tests/fundingchoices.spec.ts +7 -1
  99. package/tests/hkdir.spec.ts +10 -0
  100. package/tests/hyrabostad.spec.ts +3 -0
  101. package/tests/insided.spec.ts +9 -0
  102. package/tests/jokul-cookie-consent.spec.ts +3 -0
  103. package/tests/kartverket.spec.ts +3 -0
  104. package/tests/luxmed.spec.ts +3 -0
  105. package/tests/mars-cookies-prompt.spec.ts +3 -0
  106. package/tests/meta-cookie-dialog.spec.ts +14 -0
  107. package/tests/mittanbud.spec.ts +3 -0
  108. package/tests/niedziela-pl.spec.ts +3 -0
  109. package/tests/nikkeibp.spec.ts +8 -0
  110. package/tests/onecom-cookie-banner.spec.ts +5 -0
  111. package/tests/oslosportslager.no.spec.ts +3 -0
  112. package/tests/plex.tv.spec.ts +5 -0
  113. package/tests/poki.spec.ts +6 -0
  114. package/tests/polisen-se.spec.ts +3 -0
  115. package/tests/radio-canada.spec.ts +15 -0
  116. package/tests/regjeringen.no.spec.ts +3 -0
  117. package/tests/roll20.spec.ts +3 -0
  118. package/tests/romeo.spec.ts +3 -0
  119. package/tests/sirdata.spec.ts +13 -1
  120. package/tests/site-notifications-cookie-usage.spec.ts +3 -0
  121. package/tests/sparebank1-optin.spec.ts +8 -0
  122. package/tests/stockholm-stad.spec.ts +7 -0
  123. package/tests/stright.spec.ts +11 -0
  124. package/tests/stripchat.com.spec.ts +4 -1
  125. package/tests/tri-table.spec.ts +1 -1
  126. package/tests/tvp.spec.ts +3 -0
  127. package/tests/urvaerket.spec.ts +13 -0
  128. package/tests/vinnova-se.spec.ts +6 -0
  129. package/tests/volvocars-com.spec.ts +1 -1
  130. package/tests/vortex-consent.spec.ts +18 -0
  131. package/tests/vue-cookie-accept-decline.spec.ts +8 -0
  132. package/tests-wtr/heuristics/get-actionable-popups.ts +41 -7
  133. package/tests-wtr/heuristics/get-potential-popups.test.ts +235 -4
  134. package/tests-wtr/heuristics/heuristic-cmp.ts +16 -1
  135. package/tests-wtr/heuristics/heuristics-utils.test.ts +49 -0
  136. package/rules/generated/auto_CA_plex.tv_ee4.json +0 -40
  137. package/rules/generated/auto_CA_support.plex.tv_gn3.json +0 -40
  138. package/tests/generated/auto_CA_plex.tv_ee4.spec.ts +0 -2
  139. 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
  };
@@ -798,7 +799,15 @@ var REJECT_PATTERNS_DANISH = [
798
799
  // "nej tak" (no thanks)
799
800
  /^nej(,?\s+tak)?$/is
800
801
  ];
801
- var REJECT_PATTERNS_CATALAN = [/(^|\s)rebutj/is, "no accepto", "no, gr\xE0cies"];
802
+ var REJECT_PATTERNS_CATALAN = [
803
+ /(^|\s)rebutj/is,
804
+ "no accepto",
805
+ "no, gr\xE0cies",
806
+ // accept/allow/use (only) necessary / essential / technical / functional
807
+ /^\s*(acceptar?|accepto|permetre|utilitzar|usar)?\s*(només|sols|únicament)?\s*(les\s+|els\s+)?(galetes\s+|cookies?\s+)?(estrictament\s+)?(necess[aà]ri\w*|essencial\w*|tècni\w*|funcional\w*|imprescindibles)\s*$/is,
808
+ // continue without accepting
809
+ /^continuar sense acceptar/is
810
+ ];
802
811
  var REJECT_PATTERNS_GALICIAN = [/(^|\s)rexeitar/is];
803
812
  var REJECT_PATTERNS_BASQUE = [/(^|\s)(baztertu|ukatu)/is];
804
813
  var REJECT_PATTERNS_PORTUGUESE = [/^aceitar apenas cookies essenciais\.$/];
@@ -956,6 +965,9 @@ var SETTINGS_PATTERNS = [
956
965
  "configura-les",
957
966
  "personalitza",
958
967
  "veure prefer\xE8ncies",
968
+ /^(configuració|personalitzar|preferències|més opcions)$/,
969
+ /^(configuració|preferències|opcions)( de( les)?)? (galetes|cookies?)$/is,
970
+ /^(gestiona|gestionar|configura|configurar|personalitza|personalitzar|veure|establir|administrar)( les)?( meves)? (preferències|galetes|cookies?|opcions)$/is,
959
971
  // Galician (GL)
960
972
  "xestionar preferencias",
961
973
  // Basque (EU)
@@ -1229,6 +1241,8 @@ var ACKNOWLEDGE_PATTERNS = [
1229
1241
  "seguir",
1230
1242
  "vale",
1231
1243
  "\xA1vamos!",
1244
+ // Catalan (CA)
1245
+ /^(ho )?(entenc|entès|entesos)[ !.]*$/s,
1232
1246
  // Polish (PL)
1233
1247
  "kontynuuj",
1234
1248
  "ok, zrozumia\u0142em",
@@ -1349,28 +1363,70 @@ function classifyButtonTextRegex(buttonText) {
1349
1363
  // lib/heuristics.ts
1350
1364
  var BUTTON_LIKE_ELEMENT_SELECTOR = 'button, input[type="button"], input[type="submit"], a, [role="button"], [class*="button"]';
1351
1365
  var POPUP_SEARCH_MAX_TIME = 100;
1352
- function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME) {
1353
- const acceptedLevels = mode === "tier2" ? ["reject", "tier1", "tier2"] : mode === "tier1" ? ["reject", "tier1"] : ["reject"];
1354
- const popups = getPotentialPopups(timeout).filter((popup) => {
1355
- const popupText = popup.text?.trim();
1356
- if (!popupText || isExcludedPopup(popupText) || !hasHeuristicPatterns(popupText)) {
1357
- return false;
1358
- }
1359
- popup.regexClassification = classifyPopup(popup.buttons);
1360
- return popup.regexClassification !== void 0 && acceptedLevels.includes(popup.regexClassification);
1361
- });
1362
- 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));
1363
1377
  }
1364
- function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1378
+ function searchPopups(timeout, hitTest, hitTestGrid, stopOnReject = false) {
1365
1379
  if (!isTopFrame()) {
1366
- if (window.parent && window.parent !== window.top) {
1367
- return [];
1368
- }
1369
- const frameElement = getFramePopupElement(document);
1370
- const popup = frameElement && toPopupData(frameElement);
1371
- return popup ? [popup] : [];
1380
+ return getFramePopups();
1372
1381
  }
1373
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) {
1374
1430
  const { elements, shadowHosts } = getPopupLikeElements(deadline);
1375
1431
  const potentialPopups = [];
1376
1432
  const expanded = [];
@@ -1386,17 +1442,86 @@ function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1386
1442
  if (root !== popup) {
1387
1443
  expanded.push(root.element);
1388
1444
  }
1445
+ root.source = "walk";
1389
1446
  potentialPopups.push(root);
1390
1447
  }
1391
1448
  return potentialPopups.filter((p) => !expanded.some((root) => root !== p.element && composedContains(root, p.element)));
1392
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
+ }
1393
1518
  function toPopupData(el, shadowHosts, text = getElementText(el)) {
1394
1519
  if (!text) {
1395
1520
  return null;
1396
1521
  }
1397
1522
  const buttons = getButtonData(el, shadowHosts);
1398
1523
  classifyButtons(buttons);
1399
- return { text, element: el, buttons };
1524
+ return { text, element: el, buttons, regexClassification: classifyPopup(buttons) };
1400
1525
  }
1401
1526
  function hasPopupButtons(text, buttons) {
1402
1527
  return !isOnlyButtonText(text, buttons) && buttons.some((b) => b.regexClassification !== "other");
@@ -1408,26 +1533,34 @@ function isOnlyButtonText(text, buttons) {
1408
1533
  }
1409
1534
  return rest.replace(/\s+/g, "").length < 10;
1410
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
+ }
1411
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
+ }
1412
1558
  function expandPopupFragment(popup, shadowHosts) {
1413
1559
  if (hasHeuristicPatterns(popup.text) && hasPopupButtons(popup.text, popup.buttons)) {
1414
1560
  return popup;
1415
1561
  }
1416
- let el = getComposedParent(popup.element);
1417
- for (let depth = 0; el && depth < MAX_FRAGMENT_EXPANSION_DEPTH; depth++, el = getComposedParent(el)) {
1418
- if (el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1419
- break;
1420
- }
1421
- const text = getElementText(el);
1422
- if (!text || !hasHeuristicPatterns(text)) {
1423
- continue;
1424
- }
1425
- const root = toPopupData(el, shadowHosts, text);
1426
- if (root && hasPopupButtons(text, root.buttons)) {
1427
- return root;
1428
- }
1429
- }
1430
- return popup;
1562
+ const parent = getComposedParent(popup.element);
1563
+ return climbToPopup(parent, MAX_FRAGMENT_EXPANSION_DEPTH, (el) => toPopupRoot(el, shadowHosts)) ?? popup;
1431
1564
  }
1432
1565
  function getComposedParent(el) {
1433
1566
  if (el.parentElement) {
@@ -1458,6 +1591,9 @@ function getElementText(el) {
1458
1591
  function isRenderedTextElement(el) {
1459
1592
  return el instanceof HTMLElement && !["STYLE", "SCRIPT", "TEMPLATE", "NOSCRIPT", "LINK", "SLOT"].includes(el.tagName) && window.getComputedStyle(el).display !== "none";
1460
1593
  }
1594
+ function composedOverlaps(a, b) {
1595
+ return composedContains(a, b) || composedContains(b, a);
1596
+ }
1461
1597
  function composedContains(container, node) {
1462
1598
  if (container.contains(node)) {
1463
1599
  return true;
@@ -1949,7 +2085,11 @@ var AutoConsentHeuristicCMP = class extends AutoConsentCMPBase {
1949
2085
  async detectCmp() {
1950
2086
  await new Promise((resolve) => setTimeout(resolve, 0));
1951
2087
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorStart");
1952
- 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
+ );
1953
2093
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorEnd");
1954
2094
  this.autoconsent.config.performanceLoggingEnabled && performance.measure("heuristicDetector", "heuristicDetectorStart", "heuristicDetectorEnd");
1955
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
  };
@@ -768,7 +769,15 @@ var REJECT_PATTERNS_DANISH = [
768
769
  // "nej tak" (no thanks)
769
770
  /^nej(,?\s+tak)?$/is
770
771
  ];
771
- var REJECT_PATTERNS_CATALAN = [/(^|\s)rebutj/is, "no accepto", "no, gr\xE0cies"];
772
+ var REJECT_PATTERNS_CATALAN = [
773
+ /(^|\s)rebutj/is,
774
+ "no accepto",
775
+ "no, gr\xE0cies",
776
+ // accept/allow/use (only) necessary / essential / technical / functional
777
+ /^\s*(acceptar?|accepto|permetre|utilitzar|usar)?\s*(només|sols|únicament)?\s*(les\s+|els\s+)?(galetes\s+|cookies?\s+)?(estrictament\s+)?(necess[aà]ri\w*|essencial\w*|tècni\w*|funcional\w*|imprescindibles)\s*$/is,
778
+ // continue without accepting
779
+ /^continuar sense acceptar/is
780
+ ];
772
781
  var REJECT_PATTERNS_GALICIAN = [/(^|\s)rexeitar/is];
773
782
  var REJECT_PATTERNS_BASQUE = [/(^|\s)(baztertu|ukatu)/is];
774
783
  var REJECT_PATTERNS_PORTUGUESE = [/^aceitar apenas cookies essenciais\.$/];
@@ -926,6 +935,9 @@ var SETTINGS_PATTERNS = [
926
935
  "configura-les",
927
936
  "personalitza",
928
937
  "veure prefer\xE8ncies",
938
+ /^(configuració|personalitzar|preferències|més opcions)$/,
939
+ /^(configuració|preferències|opcions)( de( les)?)? (galetes|cookies?)$/is,
940
+ /^(gestiona|gestionar|configura|configurar|personalitza|personalitzar|veure|establir|administrar)( les)?( meves)? (preferències|galetes|cookies?|opcions)$/is,
929
941
  // Galician (GL)
930
942
  "xestionar preferencias",
931
943
  // Basque (EU)
@@ -1199,6 +1211,8 @@ var ACKNOWLEDGE_PATTERNS = [
1199
1211
  "seguir",
1200
1212
  "vale",
1201
1213
  "\xA1vamos!",
1214
+ // Catalan (CA)
1215
+ /^(ho )?(entenc|entès|entesos)[ !.]*$/s,
1202
1216
  // Polish (PL)
1203
1217
  "kontynuuj",
1204
1218
  "ok, zrozumia\u0142em",
@@ -1319,28 +1333,70 @@ function classifyButtonTextRegex(buttonText) {
1319
1333
  // lib/heuristics.ts
1320
1334
  var BUTTON_LIKE_ELEMENT_SELECTOR = 'button, input[type="button"], input[type="submit"], a, [role="button"], [class*="button"]';
1321
1335
  var POPUP_SEARCH_MAX_TIME = 100;
1322
- function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME) {
1323
- const acceptedLevels = mode === "tier2" ? ["reject", "tier1", "tier2"] : mode === "tier1" ? ["reject", "tier1"] : ["reject"];
1324
- const popups = getPotentialPopups(timeout).filter((popup) => {
1325
- const popupText = popup.text?.trim();
1326
- if (!popupText || isExcludedPopup(popupText) || !hasHeuristicPatterns(popupText)) {
1327
- return false;
1328
- }
1329
- popup.regexClassification = classifyPopup(popup.buttons);
1330
- return popup.regexClassification !== void 0 && acceptedLevels.includes(popup.regexClassification);
1331
- });
1332
- 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));
1333
1347
  }
1334
- function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1348
+ function searchPopups(timeout, hitTest, hitTestGrid, stopOnReject = false) {
1335
1349
  if (!isTopFrame()) {
1336
- if (window.parent && window.parent !== window.top) {
1337
- return [];
1338
- }
1339
- const frameElement = getFramePopupElement(document);
1340
- const popup = frameElement && toPopupData(frameElement);
1341
- return popup ? [popup] : [];
1350
+ return getFramePopups();
1342
1351
  }
1343
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) {
1344
1400
  const { elements, shadowHosts } = getPopupLikeElements(deadline);
1345
1401
  const potentialPopups = [];
1346
1402
  const expanded = [];
@@ -1356,17 +1412,86 @@ function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1356
1412
  if (root !== popup) {
1357
1413
  expanded.push(root.element);
1358
1414
  }
1415
+ root.source = "walk";
1359
1416
  potentialPopups.push(root);
1360
1417
  }
1361
1418
  return potentialPopups.filter((p) => !expanded.some((root) => root !== p.element && composedContains(root, p.element)));
1362
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
+ }
1363
1488
  function toPopupData(el, shadowHosts, text = getElementText(el)) {
1364
1489
  if (!text) {
1365
1490
  return null;
1366
1491
  }
1367
1492
  const buttons = getButtonData(el, shadowHosts);
1368
1493
  classifyButtons(buttons);
1369
- return { text, element: el, buttons };
1494
+ return { text, element: el, buttons, regexClassification: classifyPopup(buttons) };
1370
1495
  }
1371
1496
  function hasPopupButtons(text, buttons) {
1372
1497
  return !isOnlyButtonText(text, buttons) && buttons.some((b) => b.regexClassification !== "other");
@@ -1378,26 +1503,34 @@ function isOnlyButtonText(text, buttons) {
1378
1503
  }
1379
1504
  return rest.replace(/\s+/g, "").length < 10;
1380
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
+ }
1381
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
+ }
1382
1528
  function expandPopupFragment(popup, shadowHosts) {
1383
1529
  if (hasHeuristicPatterns(popup.text) && hasPopupButtons(popup.text, popup.buttons)) {
1384
1530
  return popup;
1385
1531
  }
1386
- let el = getComposedParent(popup.element);
1387
- for (let depth = 0; el && depth < MAX_FRAGMENT_EXPANSION_DEPTH; depth++, el = getComposedParent(el)) {
1388
- if (el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1389
- break;
1390
- }
1391
- const text = getElementText(el);
1392
- if (!text || !hasHeuristicPatterns(text)) {
1393
- continue;
1394
- }
1395
- const root = toPopupData(el, shadowHosts, text);
1396
- if (root && hasPopupButtons(text, root.buttons)) {
1397
- return root;
1398
- }
1399
- }
1400
- return popup;
1532
+ const parent = getComposedParent(popup.element);
1533
+ return climbToPopup(parent, MAX_FRAGMENT_EXPANSION_DEPTH, (el) => toPopupRoot(el, shadowHosts)) ?? popup;
1401
1534
  }
1402
1535
  function getComposedParent(el) {
1403
1536
  if (el.parentElement) {
@@ -1428,6 +1561,9 @@ function getElementText(el) {
1428
1561
  function isRenderedTextElement(el) {
1429
1562
  return el instanceof HTMLElement && !["STYLE", "SCRIPT", "TEMPLATE", "NOSCRIPT", "LINK", "SLOT"].includes(el.tagName) && window.getComputedStyle(el).display !== "none";
1430
1563
  }
1564
+ function composedOverlaps(a, b) {
1565
+ return composedContains(a, b) || composedContains(b, a);
1566
+ }
1431
1567
  function composedContains(container, node) {
1432
1568
  if (container.contains(node)) {
1433
1569
  return true;
@@ -1919,7 +2055,11 @@ var AutoConsentHeuristicCMP = class extends AutoConsentCMPBase {
1919
2055
  async detectCmp() {
1920
2056
  await new Promise((resolve) => setTimeout(resolve, 0));
1921
2057
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorStart");
1922
- 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
+ );
1923
2063
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorEnd");
1924
2064
  this.autoconsent.config.performanceLoggingEnabled && performance.measure("heuristicDetector", "heuristicDetectorStart", "heuristicDetectorEnd");
1925
2065
  if (this.popups.length > 0) {