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();
});