Pith - fret
fret/fret_chrome/content.js [6.7 kb]
Modified: 17:14:09 138 026 (04 Aug 026)
19 Days Ago
const CLASS = "wb-blur";
const STYLE = "filter:blur(0.35em);border-radius:2px;user-select:none;";
const SKIP_TAGS = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "IFRAME", "CANVAS"]);

let regex = null;
let words = [];
let enabled = true;
const docs = new Set([document]);
const wired = new WeakSet();
const observers = [];
const shadowRoots = new Set();
const pending = new Set();
let scheduled = false;
let reportTimer = 0;

document.documentElement.dataset.wbHost = "1";

const escapeRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

function buildRegex(words) {
  const parts = words
    .map(w => String(w).trim())
    .filter(Boolean)
    .sort((a, b) => b.length - a.length)
    .map(escapeRe);
  if (!parts.length) return null;
  return new RegExp(`(?<![\\p{L}\\p{N}_])(?:${parts.join("|")})(?![\\p{L}\\p{N}_])`, "giu");
}

const EDITABLE = "[contenteditable=''],[contenteditable='true']";

function inActiveEditor(el) {
  const host = el.closest(EDITABLE);
  if (!host) return false;
  const active = el.ownerDocument.activeElement;
  return !!active && (active === host || host.contains(active));
}

function shouldSkip(textNode) {
  const p = textNode.parentElement;
  if (!p) return true;
  if (SKIP_TAGS.has(p.tagName)) return true;
  if (p.isContentEditable && inActiveEditor(p)) return true;
  return !!p.closest?.(`.${CLASS}`);
}

function wrapMatches(node) {
  const text = node.nodeValue;
  const frag = document.createDocumentFragment();
  let last = 0, m;
  regex.lastIndex = 0;
  while ((m = regex.exec(text)) !== null) {
    if (!m[0].length) { regex.lastIndex++; continue; }
    if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index)));
    const span = document.createElement("span");
    span.className = CLASS;
    span.setAttribute("contenteditable", "false");
    span.style.cssText = STYLE.replace(/;/g, " !important;");
    span.textContent = m[0];
    frag.appendChild(span);
    last = m.index + m[0].length;
  }
  if (!last) return;
  if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
  node.parentNode.replaceChild(frag, node);
}

function frameDoc(iframe) {
  try {
    return iframe.contentDocument?.documentElement ? iframe.contentDocument : null;
  } catch {
    return null;
  }
}

function adoptFrame(iframe) {
  if (!wired.has(iframe)) {
    wired.add(iframe);
    iframe.addEventListener("load", () => {
      const d = frameDoc(iframe);
      if (d) queue([{ type: "childList", addedNodes: [d.documentElement] }]);
    });
  }
  const doc = frameDoc(iframe);
  if (!doc) return null;
  docs.add(doc);
  attachFocusHandlers(doc);
  return doc;
}

function censorRoot(root) {
  if (!root || !regex) return;
  if (root.nodeType === Node.TEXT_NODE) {
    regex.lastIndex = 0;
    if (!shouldSkip(root) && regex.test(root.nodeValue)) wrapMatches(root);
    return;
  }
  const ok = [Node.ELEMENT_NODE, Node.DOCUMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE];
  if (!ok.includes(root.nodeType)) return;

  const walker = document.createTreeWalker(
    root,
    NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT
  );
  const hits = [];
  const shadows = root.shadowRoot ? [root.shadowRoot] : [];
  const frames = [];
  let n;
  while ((n = walker.nextNode())) {
    if (n.nodeType === Node.ELEMENT_NODE) {
      if (n.shadowRoot) shadows.push(n.shadowRoot);
      if (n.tagName === "IFRAME") frames.push(n);
      continue;
    }
    if (shouldSkip(n)) continue;
    regex.lastIndex = 0;
    if (regex.test(n.nodeValue)) hits.push(n);
  }
  hits.forEach(wrapMatches);
  for (const s of shadows) {
    shadowRoots.add(s);
    censorRoot(s);
  }
  for (const f of frames) {
    const doc = adoptFrame(f);
    if (doc) censorRoot(doc.documentElement);
  }
}

function unwrapIn(root) {
  for (const span of root.querySelectorAll(`.${CLASS}`)) {
    const parent = span.parentNode;
    parent.replaceChild(document.createTextNode(span.textContent), span);
    parent.normalize();
  }
}

function unwrapAll() {
  for (const root of [...docs, ...shadowRoots]) unwrapIn(root);
}

function countSpans() {
  let n = 0;
  for (const root of [...docs, ...shadowRoots]) {
    if (root !== document && root.documentElement?.dataset.wbHost) continue;
    n += root.querySelectorAll(`.${CLASS}`).length;
  }
  return n;
}

function report() {
  clearTimeout(reportTimer);
  reportTimer = setTimeout(() => {
    chrome.runtime
      .sendMessage({ type: "wb-count", count: countSpans() })
      .catch(() => {});
  }, 250);
}

function observeRoot(root) {
  const o = new MutationObserver(queue);
  o.observe(root, { childList: true, subtree: true, characterData: true });
  observers.push(o);
}

function startObserving() {
  if (observers.length || !regex || !document.body) return;
  for (const d of docs) if (d.documentElement) observeRoot(d.documentElement);
  for (const r of shadowRoots) if (r.host?.isConnected) observeRoot(r);
}

function stopObserving() {
  observers.forEach(o => o.disconnect());
  observers.length = 0;
}

function queue(records) {
  for (const r of records) {
    if (r.type === "characterData") pending.add(r.target);
    else r.addedNodes.forEach(n => pending.add(n));
  }
  if (scheduled) return;
  scheduled = true;
  requestAnimationFrame(flush);
}

function flush() {
  scheduled = false;
  const nodes = [...pending];
  pending.clear();
  if (!regex || !nodes.length) return;
  stopObserving();
  for (const n of nodes) if (n.isConnected) censorRoot(n);
  startObserving();
  report();
}

function apply() {
  if (!document.body) return;
  stopObserving();
  pending.clear();
  for (const d of docs) if (d !== document && !d.defaultView) docs.delete(d);
  for (const r of shadowRoots) if (!r.host?.isConnected) shadowRoots.delete(r);
  unwrapAll();
  regex = enabled ? buildRegex(words) : null;
  if (regex) {
    for (const d of docs) censorRoot(d.documentElement);
    startObserving();
  }
  report();
}

function attachFocusHandlers(doc) {
  if (wired.has(doc)) return;
  wired.add(doc);
  doc.addEventListener("focusin", e => {
    const host = e.target.closest?.(EDITABLE);
    if (!host) return;
    stopObserving();
    unwrapIn(host);
    startObserving();
    report();
  }, true);
  doc.addEventListener("focusout", e => {
    const host = e.target.closest?.(EDITABLE);
    if (host) queue([{ type: "childList", addedNodes: [host] }]);
  }, true);
}

attachFocusHandlers(document);

chrome.storage.sync.get({ words: [], enabled: true }, s => {
  words = s.words;
  enabled = s.enabled;
  apply();
});

chrome.storage.onChanged.addListener((changes, area) => {
  if (area !== "sync") return;
  if (!changes.words && !changes.enabled) return;
  if (changes.words) words = changes.words.newValue ?? [];
  if (changes.enabled) enabled = changes.enabled.newValue ?? true;
  apply();
});
Updates
Kerf - Android 157.026
Kiln - Android 157.026
Wedge - Android 156.026
Whittle - Linux 155.026
Grit - Firefox/Chrome 154.026

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)
0.00151
257,016 (+270)