let currentLists = []; let isGlobalHighlightEnabled = true; let matchCase = false; let matchWhole = false; let styleSheet = null; let wordStyleMap = new Map(); function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function initializeStyleSheet() { if (!styleSheet) { const style = document.createElement('style'); style.id = 'goose-highlighter-styles'; document.head.appendChild(style); styleSheet = style.sheet; } } function updateWordStyles(activeWords) { initializeStyleSheet(); while (styleSheet.cssRules.length > 0) { styleSheet.deleteRule(0); } wordStyleMap.clear(); const uniqueStyles = new Map(); for (const word of activeWords) { const styleKey = `${word.background}-${word.foreground}`; if (!uniqueStyles.has(styleKey)) { const className = `highlighted-word-${uniqueStyles.size}`; uniqueStyles.set(styleKey, className); const rule = `.${className} { background: ${word.background}; color: ${word.foreground}; padding: 0 2px; }`; styleSheet.insertRule(rule, styleSheet.cssRules.length); } const lookup = matchCase ? word.text : word.text.toLowerCase(); wordStyleMap.set(lookup, uniqueStyles.get(styleKey)); } } function clearHighlights() { const highlightedElements = document.querySelectorAll('[data-gh]'); for (const element of highlightedElements) { const parent = element.parentNode; if (parent) { parent.replaceChild(document.createTextNode(element.textContent), element); parent.normalize(); } } } function processNodes() { observer.disconnect(); clearHighlights(); if (!isGlobalHighlightEnabled) { observer.observe(document.body, { childList: true, subtree: true, characterData: true }); return; } const textNodes = []; const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode: node => { if (node.parentNode && node.parentNode.hasAttribute('data-gh')) return NodeFilter.FILTER_REJECT; if (node.parentNode && ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME'].includes(node.parentNode.nodeName)) return NodeFilter.FILTER_REJECT; if (!node.nodeValue.trim()) return NodeFilter.FILTER_SKIP; return NodeFilter.FILTER_ACCEPT; } }); while (walker.nextNode()) textNodes.push(walker.currentNode); const activeWords = []; for (const list of currentLists) { if (!list.active) continue; for (const word of list.words) { if (!word.active) continue; activeWords.push({ text: word.wordStr, background: word.background || list.background, foreground: word.foreground || list.foreground }); } } if (activeWords.length > 0) { updateWordStyles(activeWords); const wordMap = new Map(); for (const word of activeWords) { wordMap.set(matchCase ? word.text : word.text.toLowerCase(), word); } let flags = matchCase ? 'gu' : 'giu'; let wordsPattern = Array.from(wordMap.keys()).map(escapeRegex).join('|'); if (matchWhole) { wordsPattern = `(?:(? { const lookup = matchCase ? match : match.toLowerCase(); const className = wordStyleMap.get(lookup) || 'highlighted-word-0'; return `${match}`; }); node.parentNode.replaceChild(span, node); } } catch (e) { console.error('Regex error:', e); } } observer.observe(document.body, { childList: true, subtree: true, characterData: true }); } const debouncedProcessNodes = debounce(processNodes, 300); function setListsAndUpdate(lists) { currentLists = lists; debouncedProcessNodes(); } function debounce(func, wait) { let timeout; return function () { const context = this, args = arguments; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; } // Initial highlight on load chrome.storage.local.get(['lists', 'globalHighlightEnabled', 'matchCaseEnabled', 'matchWholeEnabled'], ({ lists, globalHighlightEnabled, matchCaseEnabled, matchWholeEnabled }) => { if (Array.isArray(lists)) setListsAndUpdate(lists); if (globalHighlightEnabled !== undefined) { isGlobalHighlightEnabled = globalHighlightEnabled; } matchCase = !!matchCaseEnabled; matchWhole = !!matchWholeEnabled; processNodes(); }); // Listen for updates from the popup and re-apply highlights chrome.runtime.onMessage.addListener((message) => { if (message.type === 'WORD_LIST_UPDATED') { chrome.storage.local.get('lists', ({ lists }) => { if (Array.isArray(lists)) setListsAndUpdate(lists); }); } else if (message.type === 'GLOBAL_TOGGLE_UPDATED') { isGlobalHighlightEnabled = message.enabled; processNodes(); } else if (message.type === 'MATCH_OPTIONS_UPDATED') { matchCase = !!message.matchCase; matchWhole = !!message.matchWhole; processNodes(); } }); // Set up observer and scroll handler const observer = new MutationObserver(debouncedProcessNodes); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); window.addEventListener('scroll', debouncedProcessNodes);