CtrlK
BlogDocsLog inGet started
Tessl Logo

catalan-adobe/page-prep

Prepare any webpage for clean interaction by detecting and removing disruptive overlays (cookie banners, GDPR consent, modals, popups, newsletter signups, paywalls, login walls). Uses a cached database of 300+ known CMPs (Consent-O-Matic + EasyList) combined with heuristic DOM scanning. Produces portable JS recipes for any browser tool (Playwright, CDP, cmux-browser). ALWAYS use this skill before taking screenshots, scraping content, or automating interaction on any webpage that might have overlays blocking the view or preventing interaction. Triggers on: page prep, clean page, remove overlays, dismiss cookie banner, page blocked, overlay cleanup, consent banner, prepare page, unblock page, clear popups, cookie popup.

100

Quality

100%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

overlay-detect.jsscripts/

// overlay-detect.js — Browser-injectable overlay detection script.
// In browser context: PATTERNS is prepended by overlay-db.js bundle.
// In Node context (tests): exports internal functions.
'use strict';

function matchKnownPatterns(cmps, doc) {
  const results = [];
  for (const [name, rule] of Object.entries(cmps)) {
    for (const selector of rule.detect) {
      let el;
      try { el = doc.querySelector(selector); } catch { continue; }
      if (!el) continue;

      if (rule.detect_requires_visible) {
        const visible = el.offsetParent !== null ||
          (typeof getComputedStyle === 'function' &&
           getComputedStyle(el).display !== 'none');
        if (!visible) continue;
      }

      results.push({
        id: `overlay-${results.length}`,
        type: 'cookie-consent',
        source: 'cmp-match',
        cmp: name,
        selector,
        confidence: 1.0,
        hide: rule.hide,
        dismiss: rule.dismiss.length > 0 ? rule.dismiss : null,
      });
      break; // one match per CMP is enough
    }
  }
  return results;
}

function detectScrollLock(doc) {
  const html = doc.documentElement;
  const body = doc.body;
  if (!html || !body) return { scroll_locked: false, scroll_fix: null };
  const htmlStyle = typeof getComputedStyle === 'function'
    ? getComputedStyle(html) : html.style;
  const bodyStyle = typeof getComputedStyle === 'function'
    ? getComputedStyle(body) : body.style;
  const locked =
    htmlStyle?.overflow === 'hidden' || bodyStyle?.overflow === 'hidden';
  return {
    scroll_locked: locked,
    scroll_fix: locked
      ? 'html,body { overflow:auto!important; height:auto!important }'
      : null,
  };
}

// --- Heuristic scoring ---

const SIGNAL_WEIGHTS = {
  'high-z-index': 0.15,
  'viewport-cover': 0.25,
  'aria-modal': 0.20,
  'keyword-match': 0.15,
  'generic-selector-match': 0.10,
  'scroll-lock-boost': 0.15,
  // v2: 'has-backdrop' — detect semi-transparent siblings covering viewport
};

const OVERLAY_KEYWORDS = /cookie|consent|gdpr|modal|popup|newsletter|subscribe|paywall/i;
const CONFIDENCE_THRESHOLD = 0.30;

function scoreElement(el, computedStyle, viewport, genericSelectors, scrollLocked) {
  const signals = [];
  let confidence = 0;

  const zIndex = parseInt(computedStyle.zIndex, 10);
  if (zIndex > 999) {
    signals.push('high-z-index');
    confidence += SIGNAL_WEIGHTS['high-z-index'];
  }

  const rect = el.getBoundingClientRect();
  const vpArea = viewport.width * viewport.height;
  const coverage = vpArea > 0 ? (rect.width * rect.height) / vpArea : 0;
  if (coverage > 0.5) {
    signals.push('viewport-cover');
    confidence += SIGNAL_WEIGHTS['viewport-cover'];
  }

  const ariaModal = el.getAttribute('aria-modal');
  const role = el.getAttribute('role');
  if (ariaModal === 'true' || role === 'dialog') {
    signals.push('aria-modal');
    confidence += SIGNAL_WEIGHTS['aria-modal'];
  }

  const text = `${el.id} ${el.className}`;
  const hasKeyword = OVERLAY_KEYWORDS.test(text);
  if (hasKeyword) {
    signals.push('keyword-match');
    confidence += SIGNAL_WEIGHTS['keyword-match'];
  }

  if (scrollLocked && hasKeyword) {
    signals.push('scroll-lock-boost');
    confidence += SIGNAL_WEIGHTS['scroll-lock-boost'];
  }

  const BATCH = 50;
  for (let i = 0; i < genericSelectors.length; i += BATCH) {
    const group = genericSelectors.slice(i, i + BATCH).join(',');
    try {
      if (el.matches(group)) {
        signals.push('generic-selector-match');
        confidence += SIGNAL_WEIGHTS['generic-selector-match'];
        break;
      }
    } catch { /* invalid selector, skip */ }
  }

  return { confidence: Math.round(confidence * 100) / 100, signals };
}

function buildSelector(el) {
  const esc = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape : (s) => s;
  if (el.id) return `#${esc(el.id)}`;
  const tag = el.tagName.toLowerCase();
  const cls = el.className?.split?.(' ')?.filter(Boolean)?.[0];
  return cls ? `${tag}.${esc(cls)}` : tag;
}

function matchesAnySelector(el, selectors) {
  for (const sel of selectors) {
    try { if (el.matches(sel)) return true; } catch { /* invalid selector */ }
  }
  return false;
}

function heuristicScan(doc, genericSelectors, knownSelectors, scrollLocked) {
  const results = [];
  if (typeof doc.querySelectorAll !== 'function') return results;

  const all = doc.querySelectorAll('*');
  const viewport = {
    width: doc.documentElement?.clientWidth || 1024,
    height: doc.documentElement?.clientHeight || 768,
  };
  const seen = new Set();

  for (const el of all) {
    let style;
    try {
      style = typeof getComputedStyle === 'function'
        ? getComputedStyle(el) : el.style || {};
    } catch { continue; }

    if (style.position !== 'fixed' && style.position !== 'sticky') continue;

    const sel = buildSelector(el);
    if (seen.has(sel)) continue;
    if (matchesAnySelector(el, knownSelectors)) continue;
    seen.add(sel);

    const { confidence, signals } = scoreElement(el, style, viewport, genericSelectors, scrollLocked);
    if (confidence < CONFIDENCE_THRESHOLD) continue;

    results.push({
      id: '',
      type: 'unknown-modal',
      source: 'heuristic',
      selector: sel,
      confidence,
      signals,
      hide: [`${sel} { display:none!important }`],
      dismiss: null,
    });
  }

  return results;
}

// --- Main detection entry point ---

function detect(patterns, doc) {
  const known = matchKnownPatterns(patterns.cmps || {}, doc);
  const knownDetectSelectors = known.flatMap((o) => {
    const cmp = (patterns.cmps || {})[o.cmp];
    return cmp?.detect || [o.selector];
  });
  const scrollLock = detectScrollLock(doc);
  const heuristic = heuristicScan(doc, patterns.generic_selectors || [], knownDetectSelectors, scrollLock.scroll_locked);

  const all = [...known, ...heuristic];
  all.forEach((o, i) => { o.id = `overlay-${i}`; });

  return {
    overlays: all,
    ...scrollLock,
  };
}

// --- Module boundary ---
// In Node (tests): export internals for unit testing.
// In browser (bundled): PATTERNS is defined by the IIFE wrapper from buildBundle.
// The `return` statement returns from the IIFE, which evaluate() picks up.
if (typeof module !== 'undefined' && module.exports) {
  module.exports = { matchKnownPatterns, scoreElement, heuristicScan, matchesAnySelector, detectScrollLock, detect };
} else {
  window.__pagePrepScan = function() {
    return heuristicScan(
      document,
      PATTERNS.generic_selectors || [],
      [],
      detectScrollLock(document).scroll_locked
    );
  };
  return detect(PATTERNS, document);
}

SKILL.md

tile.json