Spaces:
Running
Running
| /** | |
| * ClauseGuard β Content Script | |
| * Page scanning + highlighting + auth bridge. | |
| * | |
| * Auth bridge: listens for postMessage from the website's ExtensionBridge component. | |
| * Content scripts CAN receive window.postMessage from the page β they share the same | |
| * window object. The message handler checks event.source === window to ensure it's | |
| * from the same page (not an iframe). | |
| */ | |
| (() => { | |
| "use strict"; | |
| if (!document.body || document.contentType !== "text/html") return; | |
| let lastScannedText = ""; | |
| let isScanning = false; | |
| let currentHighlights = []; | |
| // βββ Auth Bridge βββ | |
| // Listen for auth sync from our website (ExtensionBridge component sends this) | |
| window.addEventListener("message", (event) => { | |
| // Only accept from same window (not iframes) | |
| if (event.source !== window) return; | |
| if (!event.data || event.data.type !== "CLAUSEGUARD_AUTH_SYNC") return; | |
| const { token, email, name, userId, plan } = event.data; | |
| if (token) { | |
| // User is logged in β store auth | |
| chrome.storage.sync.set({ | |
| authToken: token, | |
| email: email || "", | |
| userName: name || "", | |
| userId: userId || "", | |
| plan: plan || "free", | |
| authSource: "website", | |
| lastSyncAt: Date.now(), | |
| }, () => { | |
| console.log("ClauseGuard: auth synced from website β", email, plan); | |
| }); | |
| } else { | |
| // User logged out β clear auth | |
| chrome.storage.sync.remove( | |
| ["authToken", "email", "userName", "userId", "plan", "authSource", "lastSyncAt"], | |
| () => { console.log("ClauseGuard: auth cleared (logout)"); } | |
| ); | |
| } | |
| }); | |
| // βββ Extract page text βββ | |
| function extractPageText() { | |
| const clone = document.body.cloneNode(true); | |
| ["script","style","nav","footer","header","aside","noscript","iframe","svg"] | |
| .forEach(tag => clone.querySelectorAll(tag).forEach(el => el.remove())); | |
| clone.querySelectorAll('[class*="cookie"],[class*="banner"],[class*="modal"],[id*="cookie"]') | |
| .forEach(el => el.remove()); | |
| return clone.innerText || clone.textContent || ""; | |
| } | |
| // βββ Scan βββ | |
| async function scanPage() { | |
| if (isScanning) return; | |
| isScanning = true; | |
| const text = extractPageText(); | |
| if (!text || text.length < 100 || text === lastScannedText) { isScanning = false; return; } | |
| lastScannedText = text; | |
| try { | |
| const results = await chrome.runtime.sendMessage({ type: "ANALYZE_TEXT", payload: { text, url: window.location.href } }); | |
| if (results && !results.error) { clearHighlights(); highlightResults(results); } | |
| } catch (err) { /* extension context invalidated β ignore */ } | |
| isScanning = false; | |
| } | |
| // βββ Highlight βββ | |
| function highlightResults(results) { | |
| if (!results.results) return; | |
| results.results.filter(r => r.categories?.length > 0).forEach(item => { | |
| if (item.text.trim().length < 20) return; | |
| findTextNodes(document.body, item.text).forEach(({ node, startOffset, endOffset }) => { | |
| wrapTextRange(node, startOffset, endOffset, item); | |
| }); | |
| }); | |
| } | |
| function findTextNodes(root, searchText) { | |
| const results = []; | |
| const searchLower = searchText.toLowerCase().substring(0, 80); | |
| const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { | |
| acceptNode(node) { | |
| if (node.parentElement?.closest(".clauseguard-highlight, script, style, nav")) return NodeFilter.FILTER_REJECT; | |
| return node.textContent.length > 20 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; | |
| }, | |
| }); | |
| let node; | |
| while ((node = walker.nextNode())) { | |
| const idx = node.textContent.toLowerCase().indexOf(searchLower); | |
| if (idx !== -1) { results.push({ node, startOffset: idx, endOffset: Math.min(idx + searchText.length, node.textContent.length) }); break; } | |
| } | |
| return results; | |
| } | |
| function wrapTextRange(textNode, start, end, clauseData) { | |
| try { | |
| const range = document.createRange(); | |
| range.setStart(textNode, start); range.setEnd(textNode, end); | |
| const severity = clauseData.categories.reduce((m, c) => ({ HIGH:3,MEDIUM:2,LOW:1 }[c.severity] > ({ HIGH:3,MEDIUM:2,LOW:1 }[m]) ? c.severity : m), "LOW"); | |
| const mark = document.createElement("mark"); | |
| mark.className = `clauseguard-highlight clauseguard-${severity.toLowerCase()}`; | |
| mark.dataset.categories = JSON.stringify(clauseData.categories); | |
| mark.addEventListener("mouseenter", showTooltip); | |
| mark.addEventListener("mouseleave", hideTooltip); | |
| mark.addEventListener("click", () => { try { chrome.runtime.sendMessage({ type: "OPEN_SIDEPANEL" }); } catch {} }); | |
| range.surroundContents(mark); | |
| currentHighlights.push(mark); | |
| } catch (e) {} | |
| } | |
| // βββ Tooltip βββ | |
| let tooltipEl = null; | |
| function showTooltip(e) { | |
| hideTooltip(); | |
| const cats = JSON.parse(e.currentTarget.dataset.categories || "[]"); | |
| if (!cats.length) return; | |
| tooltipEl = document.createElement("div"); | |
| tooltipEl.className = "clauseguard-tooltip"; | |
| tooltipEl.innerHTML = `<div class="clauseguard-tooltip-header">ClauseGuard</div>` + | |
| cats.map(c => `<div class="clauseguard-tooltip-item"><span class="clauseguard-tooltip-badge clauseguard-badge-${c.severity.toLowerCase()}">${c.severity}</span><span>${c.name}</span></div>`).join("") + | |
| `<div class="clauseguard-tooltip-footer">Click for details</div>`; | |
| document.body.appendChild(tooltipEl); | |
| const rect = e.currentTarget.getBoundingClientRect(); | |
| tooltipEl.style.left = `${rect.left + window.scrollX}px`; | |
| tooltipEl.style.top = `${rect.bottom + window.scrollY + 8}px`; | |
| } | |
| function hideTooltip() { if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; } } | |
| function clearHighlights() { | |
| currentHighlights.forEach(mark => { | |
| const p = mark.parentNode; | |
| if (p) { while (mark.firstChild) p.insertBefore(mark.firstChild, mark); p.removeChild(mark); } | |
| }); | |
| currentHighlights = []; | |
| } | |
| // βββ Auto-scan (debounced) βββ | |
| let scanTimeout = null; | |
| function debouncedScan() { clearTimeout(scanTimeout); scanTimeout = setTimeout(scanPage, 1500); } | |
| if (document.readyState === "complete") debouncedScan(); | |
| else window.addEventListener("load", debouncedScan); | |
| const observer = new MutationObserver(mutations => { | |
| if (mutations.some(m => m.addedNodes.length > 0 && [...m.addedNodes].some(n => n.nodeType === 1 && n.textContent?.length > 100))) | |
| debouncedScan(); | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| // βββ Messages from popup/sidepanel βββ | |
| chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { | |
| if (msg.type === "TRIGGER_SCAN") { lastScannedText = ""; scanPage().then(() => sendResponse({ done: true })); return true; } | |
| if (msg.type === "CLEAR_HIGHLIGHTS") { clearHighlights(); lastScannedText = ""; sendResponse({ done: true }); } | |
| if (msg.type === "GET_PAGE_TEXT") { sendResponse({ text: extractPageText(), url: window.location.href }); } | |
| }); | |
| })(); | |