Files
context-extractor/extension/content.js
T
ilia c91cd221d1
CI / Lint + tests (push) Has been cancelled
Initial Context Extractor: extension + Playwright/Camoufox package
Ship a shared markdown/prompt core used by a Brave/Chrome MV3 extension and a Python automation API, with pytest coverage, CI, and packaging smoke.
2026-07-15 14:52:22 -04:00

220 lines
7.1 KiB
JavaScript

// content.js — Context Extractor (isolated world)
//
// Loaded together with core/dom.js in the same content_scripts entry (see
// manifest.json), so getSelector() and extractMarkdown() are already in
// scope here — no import needed. Also injected on-demand by the popup via
// chrome.scripting.executeScript when a tab was open before the extension
// loaded (or after a reload of the extension without a page reload).
//
// Capture (console/fetch/XHR/errors) happens in page-patcher.js, which runs
// in the MAIN world and forwards entries here via a CustomEvent, since the
// isolated and main worlds don't share JS globals.
(() => {
// Guard against double injection (manifest auto-inject + popup inject).
if (window.__ctxExtractorIsolated) return;
window.__ctxExtractorIsolated = true;
const MAX_ENTRIES = 200;
const BRIDGE_EVENT = "__ctxExtractorEvent";
const store = {
console: [], // { level, ts, msg }
errors: [], // { type, ts, msg, source, line, col, stack }
network: [] // { type, method, url, status, ts, duration, error }
};
function push(arr, entry) {
arr.push(entry);
while (arr.length > MAX_ENTRIES) arr.shift();
}
document.addEventListener(BRIDGE_EVENT, (ev) => {
const detail = ev && ev.detail;
if (!detail) return;
const { kind, payload } = detail;
if (!payload) return;
if (kind === "console") push(store.console, payload);
else if (kind === "error") push(store.errors, payload);
else if (kind === "network") push(store.network, payload);
});
// ---------------------------------------------------------------------------
// Element picker
// ---------------------------------------------------------------------------
let pickerActive = false;
let pickerNodes = null;
function startPicker() {
if (pickerActive) return;
pickerActive = true;
const overlay = document.createElement("div");
overlay.style.cssText = [
"position:fixed", "inset:0", "z-index:2147483646",
"background:transparent", "cursor:crosshair", "pointer-events:auto"
].join(";");
const highlight = document.createElement("div");
highlight.style.cssText = [
"position:fixed", "z-index:2147483647",
"border:2px solid #4f98a3",
"background:rgba(79,152,163,0.18)",
"pointer-events:none",
"transition:all 40ms linear",
"box-sizing:border-box",
"border-radius:2px"
].join(";");
const label = document.createElement("div");
label.style.cssText = [
"position:fixed", "z-index:2147483647",
"background:#1c1b19", "color:#e8e6e3",
"font:12px/1.4 ui-monospace,Menlo,Consolas,monospace",
"padding:4px 8px", "border-radius:4px",
"border:1px solid #4f98a3",
"pointer-events:none",
"max-width:60vw", "overflow:hidden",
"text-overflow:ellipsis", "white-space:nowrap"
].join(";");
label.textContent = "Pick an element — ESC to cancel";
document.documentElement.appendChild(overlay);
document.documentElement.appendChild(highlight);
document.documentElement.appendChild(label);
let currentEl = null;
function positionHighlight(el) {
if (!el) return;
const r = el.getBoundingClientRect();
highlight.style.left = r.left + "px";
highlight.style.top = r.top + "px";
highlight.style.width = r.width + "px";
highlight.style.height = r.height + "px";
const sel = getSelector(el);
label.textContent = sel || el.tagName.toLowerCase();
const lx = Math.min(r.left, window.innerWidth - 320);
const ly = r.top > 28 ? r.top - 24 : r.bottom + 4;
label.style.left = Math.max(4, lx) + "px";
label.style.top = Math.max(4, ly) + "px";
}
function onMove(e) {
overlay.style.pointerEvents = "none";
const el = document.elementFromPoint(e.clientX, e.clientY);
overlay.style.pointerEvents = "auto";
if (el && el !== highlight && el !== label && el !== overlay) {
currentEl = el;
positionHighlight(el);
}
}
function cleanup() {
pickerActive = false;
overlay.removeEventListener("mousemove", onMove, true);
overlay.removeEventListener("click", onClick, true);
document.removeEventListener("keydown", onKey, true);
overlay.remove();
highlight.remove();
label.remove();
pickerNodes = null;
}
function onClick(e) {
e.preventDefault();
e.stopPropagation();
overlay.style.pointerEvents = "none";
const el = currentEl || document.elementFromPoint(e.clientX, e.clientY);
const sel = el ? getSelector(el) : "";
try {
chrome.runtime.sendMessage({ type: "PICKER_SELECTED", selector: sel });
} catch (_) {}
cleanup();
}
function onKey(e) {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
cleanup();
}
}
overlay.addEventListener("mousemove", onMove, true);
overlay.addEventListener("click", onClick, true);
document.addEventListener("keydown", onKey, true);
pickerNodes = { overlay, highlight, label, cleanup };
}
function stopPicker() {
if (pickerNodes && pickerNodes.cleanup) pickerNodes.cleanup();
}
// ---------------------------------------------------------------------------
// Message handling
// ---------------------------------------------------------------------------
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (!msg || !msg.type) {
sendResponse({ ok: false, error: "No message type" });
return true;
}
try {
switch (msg.type) {
case "GET_STORE": {
sendResponse({ ok: true, store });
break;
}
case "EXTRACT_CONTENT": {
const selector = (msg.selector || "").trim();
let target = null;
if (selector) {
try { target = document.querySelector(selector); } catch (_) { target = null; }
}
if (!target) target = document.body;
const markdown = extractMarkdown(target);
sendResponse({
ok: true,
meta: {
url: location.href,
title: document.title || "",
ts: Date.now(),
selector: selector || "body"
},
markdown
});
break;
}
case "START_PICKER": {
startPicker();
sendResponse({ ok: true });
break;
}
case "STOP_PICKER": {
stopPicker();
sendResponse({ ok: true });
break;
}
case "CLEAR_STORE": {
const which = msg.which;
if (which && store[which]) store[which].length = 0;
else {
store.console.length = 0;
store.errors.length = 0;
store.network.length = 0;
}
sendResponse({ ok: true });
break;
}
default:
sendResponse({ ok: false, error: "Unknown type: " + msg.type });
}
} catch (err) {
sendResponse({ ok: false, error: String(err && err.message || err) });
}
return true; // keep channel open for async
});
})();