const DEFAULT_ENDPOINT = "http://127.0.0.1:8765/screenshot"; function $(id) { return document.getElementById(id); } function setStatus(msg, kind) { const el = $("status"); el.textContent = msg; el.className = kind || ""; } async function storageGet(key) { return new Promise((resolve) => { chrome.storage.local.get([key], (res) => resolve(res[key])); }); } async function storageSet(obj) { return new Promise((resolve) => { chrome.storage.local.set(obj, () => resolve()); }); } async function getActiveTab() { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); return tabs[0] || null; } async function captureVisibleTab() { // Defaults to current window when windowId is null. return await chrome.tabs.captureVisibleTab(null, { format: "png" }); } async function extractPageContent(tabId) { const [{ result }] = await chrome.scripting.executeScript({ target: { tabId }, func: () => { const SKIP_TAGS = new Set(["script", "style", "noscript", "template", "head", "meta", "link", "svg", "canvas"]); const MAX_DEPTH = 14; const MAX_NODES = 1800; const MAX_TEXT_NODE_LEN = 500; let nodeCount = 0; let truncated = false; function cleanText(s) { return String(s || "") .replace(/\s+/g, " ") .trim(); } function rectIntersectsViewport(r) { const vw = window.innerWidth || document.documentElement.clientWidth || 0; const vh = window.innerHeight || document.documentElement.clientHeight || 0; return r.bottom > 0 && r.right > 0 && r.top < vh && r.left < vw; } function isVisibleElement(el) { try { const cs = window.getComputedStyle(el); if (!cs) return false; if (cs.display === "none" || cs.visibility === "hidden") return false; const op = Number(cs.opacity || "1"); if (!Number.isNaN(op) && op <= 0.02) return false; const r = el.getBoundingClientRect(); if ((r.width || 0) < 1 || (r.height || 0) < 1) return false; if (!rectIntersectsViewport(r)) return false; return true; } catch { return false; } } function hasMeaningfulAttrs(el) { const role = (el.getAttribute("role") || "").trim(); if (role) return true; const aria = (el.getAttribute("aria-label") || "").trim(); if (aria) return true; const dt = (el.getAttribute("data-testid") || "").trim(); if (dt) return true; const tag = el.tagName.toLowerCase(); if (tag === "a" && (el.getAttribute("href") || el.href)) return true; if (tag === "button" || tag === "input" || tag === "textarea" || tag === "select") return true; return false; } function directVisibleText(el) { // Only include immediate text nodes and form values. This avoids duplicating text up the tree. const parts = []; for (const n of Array.from(el.childNodes || [])) { if (n.nodeType === Node.TEXT_NODE) { const t = cleanText(n.nodeValue || ""); if (t) parts.push(t.length > MAX_TEXT_NODE_LEN ? t.slice(0, MAX_TEXT_NODE_LEN - 1) + "…" : t); } } const tag = el.tagName.toLowerCase(); if (tag === "input") { const type = (el.getAttribute("type") || "text").toLowerCase(); if (!["hidden", "submit", "button"].includes(type)) { const v = cleanText(el.value || el.getAttribute("value") || el.getAttribute("placeholder") || ""); if (v) parts.push(v); } } else if (tag === "textarea") { const v = cleanText(el.value || el.getAttribute("placeholder") || ""); if (v) parts.push(v); } return cleanText(parts.join(" ")); } function simplifyTag(tag) { // Keep some semantics; treat most wrapper tags as "div". if (["main", "article", "section", "ul", "ol", "li", "p"].includes(tag)) return tag; if (tag.match(/^h[1-6]$/)) return tag; if (["a", "button", "label"].includes(tag)) return tag; if (["header", "footer", "nav", "aside"].includes(tag)) return tag; return "div"; } function shouldSkip(el, tag) { if (SKIP_TAGS.has(tag)) return true; // Skip common overlay noise. const id = (el.id || "").toLowerCase(); if (id.includes("cookie") || id.includes("consent")) return true; return false; } function collapseWrappers(node) { // Collapse div/span wrappers: div > div > div ... with a single child and no text/attrs. // We only collapse when the wrapper has exactly one child. while ( node && node.tag === "div" && !node.text && !node.attrs && Array.isArray(node.children) && node.children.length === 1 && node.children[0] && node.children[0].tag ) { node = node.children[0]; } return node; } function maybeHoistInlineText(node) { // Common pattern: Text or if (!node || node.text) return node; if (!["a", "button", "label", "p", "li"].includes(node.tag)) return node; if (!Array.isArray(node.children) || node.children.length !== 1) return node; const ch = node.children[0]; if (ch && ch.text && (!ch.children || ch.children.length === 0) && (!ch.attrs || Object.keys(ch.attrs).length === 0)) { node.text = ch.text; delete node.children; } return node; } function build(el, depth) { if (truncated) return null; if (!el || depth > MAX_DEPTH) return null; if (!(el instanceof Element)) return null; const rawTag = el.tagName.toLowerCase(); if (shouldSkip(el, rawTag)) return null; if (!isVisibleElement(el)) return null; nodeCount += 1; if (nodeCount > MAX_NODES) { truncated = true; return null; } const tag = simplifyTag(rawTag); const text = directVisibleText(el); const children = []; for (const ch of Array.from(el.children || [])) { const n = build(ch, depth + 1); if (n) children.push(n); if (truncated) break; } if (!text && children.length === 0) return null; const node = { tag }; if (text) node.text = text; // Only keep attrs when they help identify purpose/action. if (hasMeaningfulAttrs(el)) { const attrs = {}; const role = (el.getAttribute("role") || "").trim(); const aria = (el.getAttribute("aria-label") || "").trim(); const dt = (el.getAttribute("data-testid") || "").trim(); if (role) attrs.role = role; if (aria) attrs.aria_label = aria; if (dt) attrs.data_testid = dt; if (rawTag === "a") { const href = (el.href || el.getAttribute("href") || "").trim(); if (href) attrs.href = href; } if (Object.keys(attrs).length) node.attrs = attrs; } if (children.length) node.children = children; return maybeHoistInlineText(collapseWrappers(node)); } const extractedAt = new Date().toISOString(); const url = String(location.href || ""); const title = String(document.title || ""); const root = document.body ? build(document.body, 0) : null; return { extracted_at: extractedAt, url, title, hostname: location.hostname, visible_tree: root, truncated, stats: { nodes: nodeCount }, }; }, }); return result; } async function postScreenshot(endpoint, payload) { const r = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const text = await r.text(); let data = null; try { data = JSON.parse(text); } catch { // ignore } if (!r.ok) { throw new Error(`HTTP ${r.status}: ${text}`); } return data; } async function ping(endpoint) { const base = endpoint.replace(/\/screenshot\s*$/, ""); const r = await fetch(`${base}/health`, { method: "GET" }); if (!r.ok) return `HTTP ${r.status}`; const j = await r.json(); return j && j.ok ? "ok" : "unexpected_response"; } async function main() { const endpointEl = $("endpoint"); const captureBtn = $("capture"); const pingBtn = $("ping"); endpointEl.value = (await storageGet("endpoint")) || DEFAULT_ENDPOINT; endpointEl.addEventListener("change", async () => { await storageSet({ endpoint: endpointEl.value.trim() }); }); pingBtn.addEventListener("click", async () => { const endpoint = endpointEl.value.trim() || DEFAULT_ENDPOINT; setStatus("Pinging...", ""); const msg = await ping(endpoint); setStatus(`Ping result: ${msg}`, msg === "ok" ? "ok" : "err"); }); captureBtn.addEventListener("click", async () => { const endpoint = endpointEl.value.trim() || DEFAULT_ENDPOINT; captureBtn.disabled = true; setStatus("Extracting page content...", ""); try { const tab = await getActiveTab(); if (!tab) throw new Error("No active tab found"); let content = null; try { content = await extractPageContent(tab.id); } catch (e) { // Some URLs (chrome://*, Web Store, PDF viewer) cannot be scripted. content = { error: String(e && e.message ? e.message : e) }; } setStatus("Capturing visible tab...", ""); const dataUrl = await captureVisibleTab(); setStatus("Uploading to local server...", ""); const resp = await postScreenshot(endpoint, { data_url: dataUrl, title: tab.title || "", url: tab.url || "", ts: new Date().toISOString(), content, }); const lines = []; lines.push("Saved:"); lines.push(` PNG: ${resp.png_path || "(unknown)"}`); lines.push(` META: ${resp.meta_path || "(unknown)"}`); if (resp.content_path) lines.push(` CONTENT: ${resp.content_path}`); if (resp.ran) { lines.push("Ran:"); if (resp.ran.error) { lines.push(` error: ${resp.ran.error}`); } else { lines.push(` exit: ${resp.ran.exit_code}`); if (resp.ran.stdout) lines.push(` stdout: ${resp.ran.stdout.trim()}`); if (resp.ran.stderr) lines.push(` stderr: ${resp.ran.stderr.trim()}`); } } setStatus(lines.join("\n"), "ok"); } catch (e) { setStatus(String(e && e.message ? e.message : e), "err"); } finally { captureBtn.disabled = false; } }); } main();