@duckduckgo/autoconsent 16.63.0 → 17.1.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 (180) hide show
  1. package/CHANGELOG.md +98 -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 +177 -38
  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 +177 -38
  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 +177 -38
  17. package/dist/autoconsent.esm.js +177 -38
  18. package/dist/autoconsent.playwright.js +180 -38
  19. package/dist/autoconsent.standalone.js +178 -39
  20. package/dist/heuristics.cjs.js +15 -3
  21. package/dist/heuristics.mjs +15 -3
  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 +18 -2
  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 +1 -1
  30. package/readme.md +1 -1
  31. package/rules/autoconsent/1pmobile.json +44 -0
  32. package/rules/autoconsent/amasty-gdpr-hyva.json +131 -0
  33. package/rules/autoconsent/amazonforum.json +86 -0
  34. package/rules/autoconsent/asrock.json +44 -0
  35. package/rules/autoconsent/babylist.json +23 -0
  36. package/rules/autoconsent/babylonbee.json +36 -0
  37. package/rules/autoconsent/bathnes-cookie-banner.json +31 -0
  38. package/rules/autoconsent/chairish.json +17 -0
  39. package/rules/autoconsent/chemicalbook.json +13 -0
  40. package/rules/autoconsent/chimnie-co-uk.json +21 -0
  41. package/rules/autoconsent/complianz-optin.json +5 -3
  42. package/rules/autoconsent/consentmo.json +5 -2
  43. package/rules/autoconsent/cookie-pref-form.json +48 -0
  44. package/rules/autoconsent/cookie-script.json +34 -15
  45. package/rules/autoconsent/endeavorhealth.json +31 -0
  46. package/rules/autoconsent/ensighten.json +3 -3
  47. package/rules/autoconsent/epaas.json +18 -1
  48. package/rules/autoconsent/fepblue-org.json +13 -0
  49. package/rules/autoconsent/fnsp-cookieconsent.json +30 -0
  50. package/rules/autoconsent/fod-fujitv.json +13 -0
  51. package/rules/autoconsent/fpl-com.json +34 -0
  52. package/rules/autoconsent/fuqer.json +11 -0
  53. package/rules/autoconsent/greatschools.json +13 -0
  54. package/rules/autoconsent/hamradio-com.json +13 -0
  55. package/rules/autoconsent/hkdir.json +33 -0
  56. package/rules/autoconsent/hubspot.json +7 -1
  57. package/rules/autoconsent/ibkr.json +16 -0
  58. package/rules/autoconsent/jetpens.json +44 -0
  59. package/rules/autoconsent/jokul-cookie-consent.json +28 -0
  60. package/rules/autoconsent/kanopy.json +88 -0
  61. package/rules/autoconsent/lego.json +27 -5
  62. package/rules/autoconsent/mainlinemenswear.json +39 -0
  63. package/rules/autoconsent/mars-cookies-prompt.json +20 -0
  64. package/rules/autoconsent/money-co-uk.json +21 -0
  65. package/rules/autoconsent/myjobquote.json +19 -0
  66. package/rules/autoconsent/niedziela-pl.json +35 -0
  67. package/rules/autoconsent/nikkeibp.json +10 -0
  68. package/rules/autoconsent/northwell-edu.json +29 -0
  69. package/rules/autoconsent/nts-org-uk.json +20 -0
  70. package/rules/autoconsent/nyu-langone.json +30 -0
  71. package/rules/autoconsent/ofsted-reports-cosmetic.json +13 -0
  72. package/rules/autoconsent/onecom-cookie-banner.json +25 -0
  73. package/rules/autoconsent/optum-legal-banner.json +10 -0
  74. package/rules/autoconsent/optum.json +35 -0
  75. package/rules/autoconsent/oslosportslager.no.json +34 -0
  76. package/rules/autoconsent/planned-parenthood.json +17 -0
  77. package/rules/autoconsent/plex.tv.json +52 -0
  78. package/rules/autoconsent/pornhub-compact-cookie-banner.json +1 -1
  79. package/rules/autoconsent/pornhub.json +1 -1
  80. package/rules/autoconsent/radio-canada.json +28 -0
  81. package/rules/autoconsent/regjeringen.no.json +13 -0
  82. package/rules/autoconsent/reviewsolicitors.json +41 -0
  83. package/rules/autoconsent/ring.json +1 -1
  84. package/rules/autoconsent/roll20.json +50 -0
  85. package/rules/autoconsent/sankei-digital.json +37 -0
  86. package/rules/autoconsent/sirdata.json +8 -3
  87. package/rules/autoconsent/site-notifications-cookie-usage.json +25 -0
  88. package/rules/autoconsent/sparebank1-optin.json +10 -0
  89. package/rules/autoconsent/texas-gov.json +22 -0
  90. package/rules/autoconsent/tiresize.com.json +17 -0
  91. package/rules/autoconsent/tri-table.json +6 -2
  92. package/rules/autoconsent/trillian.json +10 -0
  93. package/rules/autoconsent/tyreseeker.json +44 -0
  94. package/rules/autoconsent/uhcprovider.json +13 -0
  95. package/rules/autoconsent/united-online.json +66 -0
  96. package/rules/autoconsent/unitedutilities.json +52 -0
  97. package/rules/autoconsent/uttings.co.uk.json +22 -0
  98. package/rules/autoconsent/vinnova-se.json +50 -0
  99. package/rules/autoconsent/vortex-consent.json +33 -0
  100. package/rules/autoconsent/watchfinder.json +16 -0
  101. package/rules/compact-rules.json +1 -1
  102. package/rules/rule-index.json +1 -1
  103. package/rules/rules.json +1 -1
  104. package/tests/1pmobile.spec.ts +5 -0
  105. package/tests/amasty-gdpr-hyva.spec.ts +17 -0
  106. package/tests/amazonforum.spec.ts +9 -0
  107. package/tests/asrock.spec.ts +3 -0
  108. package/tests/babylist.spec.ts +3 -0
  109. package/tests/babylonbee.spec.ts +6 -0
  110. package/tests/bathnes-cookie-banner.spec.ts +3 -0
  111. package/tests/chairish.spec.ts +3 -0
  112. package/tests/chemicalbook.spec.ts +5 -0
  113. package/tests/chimnie-co-uk.spec.ts +3 -0
  114. package/tests/complianz-optin.spec.ts +2 -0
  115. package/tests/consentmo.spec.ts +2 -0
  116. package/tests/cookie-pref-form.spec.ts +8 -0
  117. package/tests/cookie-script.spec.ts +5 -0
  118. package/tests/endeavorhealth.spec.ts +3 -0
  119. package/tests/ensighten.spec.ts +7 -1
  120. package/tests/epaas.spec.ts +3 -0
  121. package/tests/fepblue-org.spec.ts +3 -0
  122. package/tests/fnsp-cookieconsent.spec.ts +9 -0
  123. package/tests/fod-fujitv.spec.ts +3 -0
  124. package/tests/fpl-com.spec.ts +3 -0
  125. package/tests/fuqer.spec.ts +3 -0
  126. package/tests/greatschools.spec.ts +3 -0
  127. package/tests/hamradio-com.spec.ts +3 -0
  128. package/tests/hkdir.spec.ts +10 -0
  129. package/tests/hubspot.spec.ts +6 -1
  130. package/tests/ibkr.spec.ts +18 -0
  131. package/tests/jetpens.spec.ts +5 -0
  132. package/tests/jokul-cookie-consent.spec.ts +3 -0
  133. package/tests/kanopy.spec.ts +5 -0
  134. package/tests/mainlinemenswear.spec.ts +3 -0
  135. package/tests/mars-cookies-prompt.spec.ts +3 -0
  136. package/tests/money-co-uk.spec.ts +3 -0
  137. package/tests/myjobquote.spec.ts +5 -0
  138. package/tests/niedziela-pl.spec.ts +3 -0
  139. package/tests/nikkeibp.spec.ts +8 -0
  140. package/tests/northwell-edu.spec.ts +3 -0
  141. package/tests/nts-org-uk.spec.ts +3 -0
  142. package/tests/nyu-langone.spec.ts +3 -0
  143. package/tests/ofsted-reports-cosmetic.spec.ts +3 -0
  144. package/tests/onecom-cookie-banner.spec.ts +5 -0
  145. package/tests/optum-legal-banner.spec.ts +9 -0
  146. package/tests/optum.spec.ts +6 -0
  147. package/tests/oslosportslager.no.spec.ts +3 -0
  148. package/tests/planned-parenthood.spec.ts +3 -0
  149. package/tests/plex.tv.spec.ts +5 -0
  150. package/tests/pornhub.spec.ts +1 -1
  151. package/tests/radio-canada.spec.ts +15 -0
  152. package/tests/regjeringen.no.spec.ts +3 -0
  153. package/tests/reviewsolicitors.spec.ts +3 -0
  154. package/tests/roll20.spec.ts +3 -0
  155. package/tests/sankei-digital.spec.ts +9 -0
  156. package/tests/sirdata.spec.ts +13 -1
  157. package/tests/site-notifications-cookie-usage.spec.ts +3 -0
  158. package/tests/sparebank1-optin.spec.ts +8 -0
  159. package/tests/texas-gov.spec.ts +3 -0
  160. package/tests/tiresize.com.spec.ts +3 -0
  161. package/tests/tri-table.spec.ts +1 -1
  162. package/tests/trillian.spec.ts +15 -0
  163. package/tests/tyreseeker.spec.ts +3 -0
  164. package/tests/uhcprovider.spec.ts +3 -0
  165. package/tests/united-online.spec.ts +4 -0
  166. package/tests/unitedutilities.spec.ts +11 -0
  167. package/tests/uttings.co.uk.spec.ts +3 -0
  168. package/tests/vinnova-se.spec.ts +6 -0
  169. package/tests/vortex-consent.spec.ts +18 -0
  170. package/tests/watchfinder.spec.ts +3 -0
  171. package/tests-wtr/heuristics/get-actionable-popups.ts +41 -7
  172. package/tests-wtr/heuristics/get-potential-popups.test.ts +235 -4
  173. package/tests-wtr/heuristics/heuristic-cmp.ts +16 -1
  174. package/tests-wtr/heuristics/heuristics-utils.test.ts +34 -4
  175. package/rules/generated/auto_CA_plex.tv_ee4.json +0 -40
  176. package/rules/generated/auto_CA_support.plex.tv_gn3.json +0 -40
  177. package/rules/generated/auto_US_interactivebrokers.com_0.json +0 -28
  178. package/tests/generated/auto_CA_plex.tv_ee4.spec.ts +0 -2
  179. package/tests/generated/auto_CA_support.plex.tv_gn3.spec.ts +0 -6
  180. package/tests/generated/auto_US_interactivebrokers.com_0.spec.ts +0 -3
@@ -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
  };
@@ -498,8 +499,8 @@ var DETECT_PATTERNS = [
498
499
  // "only necessary" is probably too broad
499
500
  /(?:by continuing.{0,100}cookie)|(?:cookie.{0,100}by continuing)/gi,
500
501
  /(?:by continuing.{0,100}privacy)|(?:privacy.{0,100}by continuing)/gi,
501
- // a single qualifier is common: "optional", "essential", "necessary", "analytics", ...
502
- /we (?:use|serve)(?: \w+)? cookies/gi,
502
+ // up to two qualifiers are common: "optional", "essential", "analytics", "google analytics", "third party", ...
503
+ /we (?:use|serve)(?: \w+){0,2} cookies/gi,
503
504
  /we are using cookies/gi,
504
505
  /use of cookies/gi,
505
506
  /website uses cookies to enhance your browsing experience/gi,
@@ -656,7 +657,10 @@ var DETECT_PATTERNS = [
656
657
  /(?:cookie|クッキー).{0,10}を(?:使用|利用)/gi,
657
658
  // Danish (DA)
658
659
  // e.g. "vi bruger cookies", "hjemmesiden bruger cookies til at ..."
659
- /bruger cookies/gi
660
+ /bruger cookies/gi,
661
+ // Norwegian (NO)
662
+ // e.g. "vi bruker informasjonskapsler", "kan vi bruke valgfrie informasjonskapsler", "nettstedet bruker cookies"
663
+ /\b(?:vi|nettstedet|nettsiden|siden)\s+bruker?\s.{0,60}?(?:informasjonskapsler|cookies)/gi
660
664
  ];
661
665
  var REJECT_PATTERNS_ENGLISH = [
662
666
  // e.g. "reject", "reject all", "reject all cookies", "deny all", "refuse cookies", "decline",
@@ -798,6 +802,14 @@ var REJECT_PATTERNS_DANISH = [
798
802
  // "nej tak" (no thanks)
799
803
  /^nej(,?\s+tak)?$/is
800
804
  ];
805
+ var REJECT_PATTERNS_NORWEGIAN = [
806
+ // avvis / avslå (reject verbs)
807
+ /(^|\s)(avvis|avslå)/is,
808
+ // (accept/allow/use) only necessary cookies
809
+ /^(godta\s+|tillat\s+|aksepter\s+|bruk\s+)?(kun|bare)\s+(strengt\s+)?nødvendige( (informasjonskapsler|cookies))?$/is,
810
+ // "nei" / "nei takk" (no thanks)
811
+ /^nei(,?\s+takk)?$/is
812
+ ];
801
813
  var REJECT_PATTERNS_CATALAN = [
802
814
  /(^|\s)rebutj/is,
803
815
  "no accepto",
@@ -853,6 +865,7 @@ var REJECT_PATTERNS = [
853
865
  ...REJECT_PATTERNS_SPANISH,
854
866
  ...REJECT_PATTERNS_SWEDISH,
855
867
  ...REJECT_PATTERNS_DANISH,
868
+ ...REJECT_PATTERNS_NORWEGIAN,
856
869
  ...REJECT_PATTERNS_CATALAN,
857
870
  ...REJECT_PATTERNS_GALICIAN,
858
871
  ...REJECT_PATTERNS_BASQUE,
@@ -1362,28 +1375,70 @@ function classifyButtonTextRegex(buttonText) {
1362
1375
  // lib/heuristics.ts
1363
1376
  var BUTTON_LIKE_ELEMENT_SELECTOR = 'button, input[type="button"], input[type="submit"], a, [role="button"], [class*="button"]';
1364
1377
  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);
1378
+ var ACCEPTED_LEVELS = {
1379
+ off: [],
1380
+ reject: ["reject"],
1381
+ tier1: ["reject", "tier1"],
1382
+ tier2: ["reject", "tier1", "tier2"]
1383
+ };
1384
+ function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME, hitTest = false) {
1385
+ const acceptedLevels = ACCEPTED_LEVELS[mode];
1386
+ const isActionable = (popup) => acceptedLevels.includes(popup.regexClassification) && isConsentPopup(popup);
1387
+ const popups = searchPopups(timeout, hitTest, DEFAULT_HIT_TEST_GRID, true).filter(isActionable);
1388
+ return popups.sort((a, b) => classificationRank(a) - classificationRank(b));
1376
1389
  }
1377
- function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1390
+ function searchPopups(timeout, hitTest, hitTestGrid, stopOnReject = false) {
1378
1391
  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] : [];
1392
+ return getFramePopups();
1385
1393
  }
1386
1394
  const deadline = performance.now() + timeout;
1395
+ const hitTestPopups = hitTest ? findPopupsByHitTest(deadline, hitTestGrid) : [];
1396
+ if (stopOnReject && hitTestPopups.some((p) => p.regexClassification === "reject" && isConsentPopup(p))) {
1397
+ return hitTestPopups;
1398
+ }
1399
+ const walkPopups = collectPositionedPopups(deadline);
1400
+ return [...walkPopups, ...mergeWalkOverlaps(hitTestPopups, walkPopups)];
1401
+ }
1402
+ var consentPopups = /* @__PURE__ */ new WeakMap();
1403
+ function classificationRank(popup) {
1404
+ return ACCEPTED_LEVELS.tier2.indexOf(popup.regexClassification);
1405
+ }
1406
+ function isConsentPopup(popup) {
1407
+ if (!ACCEPTED_LEVELS.tier2.includes(popup.regexClassification)) {
1408
+ return false;
1409
+ }
1410
+ let result = consentPopups.get(popup);
1411
+ if (result === void 0) {
1412
+ const popupText = popup.text.trim();
1413
+ result = !!popupText && !isExcludedPopup(popupText) && hasHeuristicPatterns(popupText);
1414
+ consentPopups.set(popup, result);
1415
+ }
1416
+ return result;
1417
+ }
1418
+ function mergeWalkOverlaps(hitTestPopups, walkPopups) {
1419
+ return hitTestPopups.filter((h) => {
1420
+ const hitIsConsent = isConsentPopup(h);
1421
+ const absorbs = (p) => !hitIsConsent || isConsentPopup(p) && classificationRank(p) <= classificationRank(h);
1422
+ const walkPopup = walkPopups.find((p) => composedOverlaps(p.element, h.element) && absorbs(p));
1423
+ if (walkPopup) {
1424
+ (walkPopup.hitPoints ??= []).push(...h.hitPoints ?? []);
1425
+ }
1426
+ return !walkPopup;
1427
+ });
1428
+ }
1429
+ function getFramePopups() {
1430
+ if (window.parent && window.parent !== window.top) {
1431
+ return [];
1432
+ }
1433
+ const frameElement = getFramePopupElement(document);
1434
+ const popup = frameElement && toPopupData(frameElement);
1435
+ if (!popup) {
1436
+ return [];
1437
+ }
1438
+ popup.source = "frame";
1439
+ return [popup];
1440
+ }
1441
+ function collectPositionedPopups(deadline) {
1387
1442
  const { elements, shadowHosts } = getPopupLikeElements(deadline);
1388
1443
  const potentialPopups = [];
1389
1444
  const expanded = [];
@@ -1399,17 +1454,86 @@ function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1399
1454
  if (root !== popup) {
1400
1455
  expanded.push(root.element);
1401
1456
  }
1457
+ root.source = "walk";
1402
1458
  potentialPopups.push(root);
1403
1459
  }
1404
1460
  return potentialPopups.filter((p) => !expanded.some((root) => root !== p.element && composedContains(root, p.element)));
1405
1461
  }
1462
+ var DEFAULT_HIT_TEST_GRID = {
1463
+ columns: [0.1, 0.5, 0.9],
1464
+ // banners sit along the top and bottom edges and modals in the middle; rows 150px in added almost nothing in crawls
1465
+ edgeRows: [30, 90]
1466
+ };
1467
+ function findPopupsByHitTest(deadline, grid) {
1468
+ const climbed = /* @__PURE__ */ new Map();
1469
+ const found = [];
1470
+ const toPopup = (el) => el.querySelector(TEXT_ENTRY_SELECTOR) ? null : toPopupRoot(el);
1471
+ for (const [x, y] of getHitTestPoints(grid)) {
1472
+ if (performance.now() > deadline) {
1473
+ return found;
1474
+ }
1475
+ const path = [];
1476
+ let reached = null;
1477
+ const shouldStop = (el) => {
1478
+ if (climbed.has(el)) {
1479
+ reached = climbed.get(el) ?? null;
1480
+ return true;
1481
+ }
1482
+ const overlapped = found.find((p) => composedOverlaps(p.element, el));
1483
+ if (overlapped) {
1484
+ reached = composedContains(overlapped.element, el) ? overlapped : null;
1485
+ return true;
1486
+ }
1487
+ path.push(el);
1488
+ return false;
1489
+ };
1490
+ const hit = closestHTMLElement(deepElementFromPoint(x, y));
1491
+ const popup = climbToPopup(hit, MAX_HIT_TEST_CLIMB_DEPTH, toPopup, shouldStop);
1492
+ if (popup) {
1493
+ popup.source = "hitTest";
1494
+ found.push(popup);
1495
+ reached = popup;
1496
+ }
1497
+ for (const el of path) {
1498
+ climbed.set(el, reached);
1499
+ }
1500
+ if (reached) {
1501
+ (reached.hitPoints ??= []).push([x, y]);
1502
+ }
1503
+ }
1504
+ return found;
1505
+ }
1506
+ function getHitTestPoints(grid) {
1507
+ const { innerWidth: width, innerHeight: height } = window;
1508
+ const rows = [...grid.edgeRows, height / 2, ...grid.edgeRows.map((y) => height - y)].map((y) => Math.round(y));
1509
+ const ys = [...new Set(rows)].filter((y) => y >= 0 && y < height);
1510
+ return ys.flatMap((y) => grid.columns.map((column) => [Math.round(column * width), y]));
1511
+ }
1512
+ 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"]';
1513
+ function deepElementFromPoint(x, y) {
1514
+ let el = document.elementFromPoint(x, y);
1515
+ while (el?.shadowRoot) {
1516
+ const inner = el.shadowRoot.elementFromPoint(x, y);
1517
+ if (!inner || inner === el) {
1518
+ break;
1519
+ }
1520
+ el = inner;
1521
+ }
1522
+ return el;
1523
+ }
1524
+ function closestHTMLElement(el) {
1525
+ while (el && !(el instanceof HTMLElement)) {
1526
+ el = el.parentElement;
1527
+ }
1528
+ return el;
1529
+ }
1406
1530
  function toPopupData(el, shadowHosts, text = getElementText(el)) {
1407
1531
  if (!text) {
1408
1532
  return null;
1409
1533
  }
1410
1534
  const buttons = getButtonData(el, shadowHosts);
1411
1535
  classifyButtons(buttons);
1412
- return { text, element: el, buttons };
1536
+ return { text, element: el, buttons, regexClassification: classifyPopup(buttons) };
1413
1537
  }
1414
1538
  function hasPopupButtons(text, buttons) {
1415
1539
  return !isOnlyButtonText(text, buttons) && buttons.some((b) => b.regexClassification !== "other");
@@ -1421,26 +1545,34 @@ function isOnlyButtonText(text, buttons) {
1421
1545
  }
1422
1546
  return rest.replace(/\s+/g, "").length < 10;
1423
1547
  }
1548
+ function toPopupRoot(el, shadowHosts) {
1549
+ const text = getElementText(el);
1550
+ if (!text || !hasHeuristicPatterns(text)) {
1551
+ return null;
1552
+ }
1553
+ const popup = toPopupData(el, shadowHosts, text);
1554
+ return popup && hasPopupButtons(text, popup.buttons) ? popup : null;
1555
+ }
1424
1556
  var MAX_FRAGMENT_EXPANSION_DEPTH = 6;
1557
+ var MAX_HIT_TEST_CLIMB_DEPTH = 8;
1558
+ function climbToPopup(el, maxDepth, toPopup, shouldStop) {
1559
+ for (let depth = 0; el && depth < maxDepth; depth++, el = getComposedParent(el)) {
1560
+ if (shouldStop?.(el) || el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1561
+ return null;
1562
+ }
1563
+ const popup = toPopup(el);
1564
+ if (popup) {
1565
+ return popup;
1566
+ }
1567
+ }
1568
+ return null;
1569
+ }
1425
1570
  function expandPopupFragment(popup, shadowHosts) {
1426
1571
  if (hasHeuristicPatterns(popup.text) && hasPopupButtons(popup.text, popup.buttons)) {
1427
1572
  return popup;
1428
1573
  }
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;
1574
+ const parent = getComposedParent(popup.element);
1575
+ return climbToPopup(parent, MAX_FRAGMENT_EXPANSION_DEPTH, (el) => toPopupRoot(el, shadowHosts)) ?? popup;
1444
1576
  }
1445
1577
  function getComposedParent(el) {
1446
1578
  if (el.parentElement) {
@@ -1471,6 +1603,9 @@ function getElementText(el) {
1471
1603
  function isRenderedTextElement(el) {
1472
1604
  return el instanceof HTMLElement && !["STYLE", "SCRIPT", "TEMPLATE", "NOSCRIPT", "LINK", "SLOT"].includes(el.tagName) && window.getComputedStyle(el).display !== "none";
1473
1605
  }
1606
+ function composedOverlaps(a, b) {
1607
+ return composedContains(a, b) || composedContains(b, a);
1608
+ }
1474
1609
  function composedContains(container, node) {
1475
1610
  if (container.contains(node)) {
1476
1611
  return true;
@@ -1962,7 +2097,11 @@ var AutoConsentHeuristicCMP = class extends AutoConsentCMPBase {
1962
2097
  async detectCmp() {
1963
2098
  await new Promise((resolve) => setTimeout(resolve, 0));
1964
2099
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorStart");
1965
- this.popups = getActionablePopups(this.mode, this.autoconsent.config.heuristicPopupSearchTimeout);
2100
+ this.popups = getActionablePopups(
2101
+ this.mode,
2102
+ this.autoconsent.config.heuristicPopupSearchTimeout,
2103
+ this.autoconsent.config.enableHeuristicHitTest
2104
+ );
1966
2105
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorEnd");
1967
2106
  this.autoconsent.config.performanceLoggingEnabled && performance.measure("heuristicDetector", "heuristicDetectorStart", "heuristicDetectorEnd");
1968
2107
  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
  };
@@ -468,8 +469,8 @@ var DETECT_PATTERNS = [
468
469
  // "only necessary" is probably too broad
469
470
  /(?:by continuing.{0,100}cookie)|(?:cookie.{0,100}by continuing)/gi,
470
471
  /(?:by continuing.{0,100}privacy)|(?:privacy.{0,100}by continuing)/gi,
471
- // a single qualifier is common: "optional", "essential", "necessary", "analytics", ...
472
- /we (?:use|serve)(?: \w+)? cookies/gi,
472
+ // up to two qualifiers are common: "optional", "essential", "analytics", "google analytics", "third party", ...
473
+ /we (?:use|serve)(?: \w+){0,2} cookies/gi,
473
474
  /we are using cookies/gi,
474
475
  /use of cookies/gi,
475
476
  /website uses cookies to enhance your browsing experience/gi,
@@ -626,7 +627,10 @@ var DETECT_PATTERNS = [
626
627
  /(?:cookie|クッキー).{0,10}を(?:使用|利用)/gi,
627
628
  // Danish (DA)
628
629
  // e.g. "vi bruger cookies", "hjemmesiden bruger cookies til at ..."
629
- /bruger cookies/gi
630
+ /bruger cookies/gi,
631
+ // Norwegian (NO)
632
+ // e.g. "vi bruker informasjonskapsler", "kan vi bruke valgfrie informasjonskapsler", "nettstedet bruker cookies"
633
+ /\b(?:vi|nettstedet|nettsiden|siden)\s+bruker?\s.{0,60}?(?:informasjonskapsler|cookies)/gi
630
634
  ];
631
635
  var REJECT_PATTERNS_ENGLISH = [
632
636
  // e.g. "reject", "reject all", "reject all cookies", "deny all", "refuse cookies", "decline",
@@ -768,6 +772,14 @@ var REJECT_PATTERNS_DANISH = [
768
772
  // "nej tak" (no thanks)
769
773
  /^nej(,?\s+tak)?$/is
770
774
  ];
775
+ var REJECT_PATTERNS_NORWEGIAN = [
776
+ // avvis / avslå (reject verbs)
777
+ /(^|\s)(avvis|avslå)/is,
778
+ // (accept/allow/use) only necessary cookies
779
+ /^(godta\s+|tillat\s+|aksepter\s+|bruk\s+)?(kun|bare)\s+(strengt\s+)?nødvendige( (informasjonskapsler|cookies))?$/is,
780
+ // "nei" / "nei takk" (no thanks)
781
+ /^nei(,?\s+takk)?$/is
782
+ ];
771
783
  var REJECT_PATTERNS_CATALAN = [
772
784
  /(^|\s)rebutj/is,
773
785
  "no accepto",
@@ -823,6 +835,7 @@ var REJECT_PATTERNS = [
823
835
  ...REJECT_PATTERNS_SPANISH,
824
836
  ...REJECT_PATTERNS_SWEDISH,
825
837
  ...REJECT_PATTERNS_DANISH,
838
+ ...REJECT_PATTERNS_NORWEGIAN,
826
839
  ...REJECT_PATTERNS_CATALAN,
827
840
  ...REJECT_PATTERNS_GALICIAN,
828
841
  ...REJECT_PATTERNS_BASQUE,
@@ -1332,28 +1345,70 @@ function classifyButtonTextRegex(buttonText) {
1332
1345
  // lib/heuristics.ts
1333
1346
  var BUTTON_LIKE_ELEMENT_SELECTOR = 'button, input[type="button"], input[type="submit"], a, [role="button"], [class*="button"]';
1334
1347
  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);
1348
+ var ACCEPTED_LEVELS = {
1349
+ off: [],
1350
+ reject: ["reject"],
1351
+ tier1: ["reject", "tier1"],
1352
+ tier2: ["reject", "tier1", "tier2"]
1353
+ };
1354
+ function getActionablePopups(mode = "reject", timeout = POPUP_SEARCH_MAX_TIME, hitTest = false) {
1355
+ const acceptedLevels = ACCEPTED_LEVELS[mode];
1356
+ const isActionable = (popup) => acceptedLevels.includes(popup.regexClassification) && isConsentPopup(popup);
1357
+ const popups = searchPopups(timeout, hitTest, DEFAULT_HIT_TEST_GRID, true).filter(isActionable);
1358
+ return popups.sort((a, b) => classificationRank(a) - classificationRank(b));
1346
1359
  }
1347
- function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1360
+ function searchPopups(timeout, hitTest, hitTestGrid, stopOnReject = false) {
1348
1361
  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] : [];
1362
+ return getFramePopups();
1355
1363
  }
1356
1364
  const deadline = performance.now() + timeout;
1365
+ const hitTestPopups = hitTest ? findPopupsByHitTest(deadline, hitTestGrid) : [];
1366
+ if (stopOnReject && hitTestPopups.some((p) => p.regexClassification === "reject" && isConsentPopup(p))) {
1367
+ return hitTestPopups;
1368
+ }
1369
+ const walkPopups = collectPositionedPopups(deadline);
1370
+ return [...walkPopups, ...mergeWalkOverlaps(hitTestPopups, walkPopups)];
1371
+ }
1372
+ var consentPopups = /* @__PURE__ */ new WeakMap();
1373
+ function classificationRank(popup) {
1374
+ return ACCEPTED_LEVELS.tier2.indexOf(popup.regexClassification);
1375
+ }
1376
+ function isConsentPopup(popup) {
1377
+ if (!ACCEPTED_LEVELS.tier2.includes(popup.regexClassification)) {
1378
+ return false;
1379
+ }
1380
+ let result = consentPopups.get(popup);
1381
+ if (result === void 0) {
1382
+ const popupText = popup.text.trim();
1383
+ result = !!popupText && !isExcludedPopup(popupText) && hasHeuristicPatterns(popupText);
1384
+ consentPopups.set(popup, result);
1385
+ }
1386
+ return result;
1387
+ }
1388
+ function mergeWalkOverlaps(hitTestPopups, walkPopups) {
1389
+ return hitTestPopups.filter((h) => {
1390
+ const hitIsConsent = isConsentPopup(h);
1391
+ const absorbs = (p) => !hitIsConsent || isConsentPopup(p) && classificationRank(p) <= classificationRank(h);
1392
+ const walkPopup = walkPopups.find((p) => composedOverlaps(p.element, h.element) && absorbs(p));
1393
+ if (walkPopup) {
1394
+ (walkPopup.hitPoints ??= []).push(...h.hitPoints ?? []);
1395
+ }
1396
+ return !walkPopup;
1397
+ });
1398
+ }
1399
+ function getFramePopups() {
1400
+ if (window.parent && window.parent !== window.top) {
1401
+ return [];
1402
+ }
1403
+ const frameElement = getFramePopupElement(document);
1404
+ const popup = frameElement && toPopupData(frameElement);
1405
+ if (!popup) {
1406
+ return [];
1407
+ }
1408
+ popup.source = "frame";
1409
+ return [popup];
1410
+ }
1411
+ function collectPositionedPopups(deadline) {
1357
1412
  const { elements, shadowHosts } = getPopupLikeElements(deadline);
1358
1413
  const potentialPopups = [];
1359
1414
  const expanded = [];
@@ -1369,17 +1424,86 @@ function getPotentialPopups(timeout = POPUP_SEARCH_MAX_TIME) {
1369
1424
  if (root !== popup) {
1370
1425
  expanded.push(root.element);
1371
1426
  }
1427
+ root.source = "walk";
1372
1428
  potentialPopups.push(root);
1373
1429
  }
1374
1430
  return potentialPopups.filter((p) => !expanded.some((root) => root !== p.element && composedContains(root, p.element)));
1375
1431
  }
1432
+ var DEFAULT_HIT_TEST_GRID = {
1433
+ columns: [0.1, 0.5, 0.9],
1434
+ // banners sit along the top and bottom edges and modals in the middle; rows 150px in added almost nothing in crawls
1435
+ edgeRows: [30, 90]
1436
+ };
1437
+ function findPopupsByHitTest(deadline, grid) {
1438
+ const climbed = /* @__PURE__ */ new Map();
1439
+ const found = [];
1440
+ const toPopup = (el) => el.querySelector(TEXT_ENTRY_SELECTOR) ? null : toPopupRoot(el);
1441
+ for (const [x, y] of getHitTestPoints(grid)) {
1442
+ if (performance.now() > deadline) {
1443
+ return found;
1444
+ }
1445
+ const path = [];
1446
+ let reached = null;
1447
+ const shouldStop = (el) => {
1448
+ if (climbed.has(el)) {
1449
+ reached = climbed.get(el) ?? null;
1450
+ return true;
1451
+ }
1452
+ const overlapped = found.find((p) => composedOverlaps(p.element, el));
1453
+ if (overlapped) {
1454
+ reached = composedContains(overlapped.element, el) ? overlapped : null;
1455
+ return true;
1456
+ }
1457
+ path.push(el);
1458
+ return false;
1459
+ };
1460
+ const hit = closestHTMLElement(deepElementFromPoint(x, y));
1461
+ const popup = climbToPopup(hit, MAX_HIT_TEST_CLIMB_DEPTH, toPopup, shouldStop);
1462
+ if (popup) {
1463
+ popup.source = "hitTest";
1464
+ found.push(popup);
1465
+ reached = popup;
1466
+ }
1467
+ for (const el of path) {
1468
+ climbed.set(el, reached);
1469
+ }
1470
+ if (reached) {
1471
+ (reached.hitPoints ??= []).push([x, y]);
1472
+ }
1473
+ }
1474
+ return found;
1475
+ }
1476
+ function getHitTestPoints(grid) {
1477
+ const { innerWidth: width, innerHeight: height } = window;
1478
+ const rows = [...grid.edgeRows, height / 2, ...grid.edgeRows.map((y) => height - y)].map((y) => Math.round(y));
1479
+ const ys = [...new Set(rows)].filter((y) => y >= 0 && y < height);
1480
+ return ys.flatMap((y) => grid.columns.map((column) => [Math.round(column * width), y]));
1481
+ }
1482
+ 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"]';
1483
+ function deepElementFromPoint(x, y) {
1484
+ let el = document.elementFromPoint(x, y);
1485
+ while (el?.shadowRoot) {
1486
+ const inner = el.shadowRoot.elementFromPoint(x, y);
1487
+ if (!inner || inner === el) {
1488
+ break;
1489
+ }
1490
+ el = inner;
1491
+ }
1492
+ return el;
1493
+ }
1494
+ function closestHTMLElement(el) {
1495
+ while (el && !(el instanceof HTMLElement)) {
1496
+ el = el.parentElement;
1497
+ }
1498
+ return el;
1499
+ }
1376
1500
  function toPopupData(el, shadowHosts, text = getElementText(el)) {
1377
1501
  if (!text) {
1378
1502
  return null;
1379
1503
  }
1380
1504
  const buttons = getButtonData(el, shadowHosts);
1381
1505
  classifyButtons(buttons);
1382
- return { text, element: el, buttons };
1506
+ return { text, element: el, buttons, regexClassification: classifyPopup(buttons) };
1383
1507
  }
1384
1508
  function hasPopupButtons(text, buttons) {
1385
1509
  return !isOnlyButtonText(text, buttons) && buttons.some((b) => b.regexClassification !== "other");
@@ -1391,26 +1515,34 @@ function isOnlyButtonText(text, buttons) {
1391
1515
  }
1392
1516
  return rest.replace(/\s+/g, "").length < 10;
1393
1517
  }
1518
+ function toPopupRoot(el, shadowHosts) {
1519
+ const text = getElementText(el);
1520
+ if (!text || !hasHeuristicPatterns(text)) {
1521
+ return null;
1522
+ }
1523
+ const popup = toPopupData(el, shadowHosts, text);
1524
+ return popup && hasPopupButtons(text, popup.buttons) ? popup : null;
1525
+ }
1394
1526
  var MAX_FRAGMENT_EXPANSION_DEPTH = 6;
1527
+ var MAX_HIT_TEST_CLIMB_DEPTH = 8;
1528
+ function climbToPopup(el, maxDepth, toPopup, shouldStop) {
1529
+ for (let depth = 0; el && depth < maxDepth; depth++, el = getComposedParent(el)) {
1530
+ if (shouldStop?.(el) || el === document.body || el === document.documentElement || el.getBoundingClientRect().height > window.innerHeight) {
1531
+ return null;
1532
+ }
1533
+ const popup = toPopup(el);
1534
+ if (popup) {
1535
+ return popup;
1536
+ }
1537
+ }
1538
+ return null;
1539
+ }
1395
1540
  function expandPopupFragment(popup, shadowHosts) {
1396
1541
  if (hasHeuristicPatterns(popup.text) && hasPopupButtons(popup.text, popup.buttons)) {
1397
1542
  return popup;
1398
1543
  }
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;
1544
+ const parent = getComposedParent(popup.element);
1545
+ return climbToPopup(parent, MAX_FRAGMENT_EXPANSION_DEPTH, (el) => toPopupRoot(el, shadowHosts)) ?? popup;
1414
1546
  }
1415
1547
  function getComposedParent(el) {
1416
1548
  if (el.parentElement) {
@@ -1441,6 +1573,9 @@ function getElementText(el) {
1441
1573
  function isRenderedTextElement(el) {
1442
1574
  return el instanceof HTMLElement && !["STYLE", "SCRIPT", "TEMPLATE", "NOSCRIPT", "LINK", "SLOT"].includes(el.tagName) && window.getComputedStyle(el).display !== "none";
1443
1575
  }
1576
+ function composedOverlaps(a, b) {
1577
+ return composedContains(a, b) || composedContains(b, a);
1578
+ }
1444
1579
  function composedContains(container, node) {
1445
1580
  if (container.contains(node)) {
1446
1581
  return true;
@@ -1932,7 +2067,11 @@ var AutoConsentHeuristicCMP = class extends AutoConsentCMPBase {
1932
2067
  async detectCmp() {
1933
2068
  await new Promise((resolve) => setTimeout(resolve, 0));
1934
2069
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorStart");
1935
- this.popups = getActionablePopups(this.mode, this.autoconsent.config.heuristicPopupSearchTimeout);
2070
+ this.popups = getActionablePopups(
2071
+ this.mode,
2072
+ this.autoconsent.config.heuristicPopupSearchTimeout,
2073
+ this.autoconsent.config.enableHeuristicHitTest
2074
+ );
1936
2075
  this.autoconsent.config.performanceLoggingEnabled && performance.mark("heuristicDetectorEnd");
1937
2076
  this.autoconsent.config.performanceLoggingEnabled && performance.measure("heuristicDetector", "heuristicDetectorStart", "heuristicDetectorEnd");
1938
2077
  if (this.popups.length > 0) {