fortified selector logic, [bug] selector can't process complex queries

This commit is contained in:
2026-01-18 09:29:24 -05:00
parent d30b7de7d9
commit 488138a389
4 changed files with 117 additions and 9 deletions

View File

@@ -22,6 +22,45 @@ function findMinimumScope(text) {
return deepest; return deepest;
} }
function escapeSelector(value) {
if (window.CSS && typeof CSS.escape === "function") {
return CSS.escape(value);
}
return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
}
function buildSelector(node) {
if (!node || node.nodeType !== 1) return "body";
if (node === document.body) return "body";
const parts = [];
let current = node;
while (current && current.nodeType === 1 && current !== document.body) {
const tag = current.tagName.toLowerCase();
if (current.id) {
parts.unshift(`${tag}#${escapeSelector(current.id)}`);
break;
}
let selector = tag;
const parent = current.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter(
(child) => child.tagName === current.tagName
);
if (siblings.length > 1) {
const index = siblings.indexOf(current) + 1;
selector += `:nth-of-type(${index})`;
}
}
parts.unshift(selector);
current = parent;
}
if (current === document.body) {
parts.unshift("body");
}
const selector = parts.join(" > ");
return selector || "body";
}
function normalizeName(value) { function normalizeName(value) {
return (value || "").trim().toLowerCase(); return (value || "").trim().toLowerCase();
} }
@@ -274,12 +313,36 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
sendResponse({ ok: false, error: "Scope not found." }); sendResponse({ ok: false, error: "Scope not found." });
return; return;
} }
sendResponse({ ok: true, extracted: node.innerText || "" }); sendResponse({
ok: true,
extracted: node.innerText || "",
selector: buildSelector(node)
});
return;
}
if (message.type === "EXTRACT_BY_SELECTOR") {
const selector = message.selector || "";
if (!selector) {
sendResponse({ ok: false, error: "Missing selector." });
return;
}
let node = null;
try {
node = document.querySelector(selector);
} catch {
sendResponse({ ok: false, error: "Invalid selector." });
return;
}
if (!node) {
sendResponse({ ok: false, error: "Selector not found." });
return;
}
sendResponse({ ok: true, extracted: node.innerText || "", selector });
return; return;
} }
if (message.type === "EXTRACT_FULL") { if (message.type === "EXTRACT_FULL") {
const extracted = document.body?.innerText || ""; const extracted = document.body?.innerText || "";
sendResponse({ ok: true, extracted }); sendResponse({ ok: true, extracted, selector: "body" });
} }
}); });

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "SiteCompanion", "name": "SiteCompanion",
"version": "0.4.0", "version": "0.4.1",
"description": "AI companion for site-bound text extraction and tasks.", "description": "AI companion for site-bound text extraction and tasks.",
"permissions": ["storage", "activeTab"], "permissions": ["storage", "activeTab"],
"host_permissions": ["<all_urls>"], "host_permissions": ["<all_urls>"],

View File

@@ -49,6 +49,7 @@ const state = {
currentPopupState: "unknown", currentPopupState: "unknown",
globalTheme: "system", globalTheme: "system",
forcedTask: null, forcedTask: null,
siteTextSelector: "",
selectedTaskId: "", selectedTaskId: "",
selectedEnvId: "", selectedEnvId: "",
selectedProfileId: "" selectedProfileId: ""
@@ -75,7 +76,8 @@ function buildPopupDraft() {
state: state.currentPopupState, state: state.currentPopupState,
siteText: state.siteText || "", siteText: state.siteText || "",
urlPattern: urlPatternInput?.value?.trim() || "", urlPattern: urlPatternInput?.value?.trim() || "",
siteName: siteNameInput?.value?.trim() || "" siteName: siteNameInput?.value?.trim() || "",
siteTextSelector: state.siteTextSelector || ""
}; };
} }
@@ -99,6 +101,9 @@ function applyPopupDraft(draft) {
if (typeof draft.siteName === "string") { if (typeof draft.siteName === "string") {
siteNameInput.value = draft.siteName; siteNameInput.value = draft.siteName;
} }
if (typeof draft.siteTextSelector === "string") {
state.siteTextSelector = draft.siteTextSelector;
}
} }
function matchUrl(url, pattern) { function matchUrl(url, pattern) {
@@ -190,10 +195,14 @@ function filterApiConfigsForScope(apiConfigs, workspace, site) {
async function detectSite(url) { async function detectSite(url) {
const { sites = [], workspaces = [] } = await getStorage(["sites", "workspaces"]); const { sites = [], workspaces = [] } = await getStorage(["sites", "workspaces"]);
state.sites = sites; const normalizedSites = (Array.isArray(sites) ? sites : []).map((site) => ({
...site,
extractSelector: site?.extractSelector || "body"
}));
state.sites = normalizedSites;
state.workspaces = workspaces; state.workspaces = workspaces;
const site = sites.find(s => matchUrl(url, s.urlPattern)); const site = normalizedSites.find((s) => matchUrl(url, s.urlPattern));
if (site) { if (site) {
state.currentSite = site; state.currentSite = site;
const workspace = const workspace =
@@ -695,7 +704,12 @@ async function loadConfig() {
const envs = normalizeConfigList(stored.envConfigs); const envs = normalizeConfigList(stored.envConfigs);
const profiles = normalizeConfigList(stored.profiles); const profiles = normalizeConfigList(stored.profiles);
const shortcuts = normalizeConfigList(stored.shortcuts); const shortcuts = normalizeConfigList(stored.shortcuts);
const sites = Array.isArray(stored.sites) ? stored.sites : state.sites; const sites = Array.isArray(stored.sites)
? stored.sites.map((site) => ({
...site,
extractSelector: site?.extractSelector || "body"
}))
: state.sites;
const workspaces = Array.isArray(stored.workspaces) const workspaces = Array.isArray(stored.workspaces)
? stored.workspaces ? stored.workspaces
: state.workspaces; : state.workspaces;
@@ -709,6 +723,9 @@ async function loadConfig() {
activeSite && activeSite.workspaceId activeSite && activeSite.workspaceId
? workspaces.find((entry) => entry.id === activeSite.workspaceId) ? workspaces.find((entry) => entry.id === activeSite.workspaceId)
: null; : null;
if (activeSite) {
state.currentSite = activeSite;
}
if (activeWorkspace) { if (activeWorkspace) {
state.currentWorkspace = activeWorkspace; state.currentWorkspace = activeWorkspace;
currentWorkspaceName.textContent = activeWorkspace.name || "Global"; currentWorkspaceName.textContent = activeWorkspace.name || "Global";
@@ -795,13 +812,18 @@ async function loadTheme() {
async function handleExtract() { async function handleExtract() {
setStatus("Extracting..."); setStatus("Extracting...");
try { try {
const response = await sendToActiveTab({ type: "EXTRACT_FULL" }); const selector = state.currentSite?.extractSelector || "body";
const response = await sendToActiveTab({
type: "EXTRACT_BY_SELECTOR",
selector
});
if (!response?.ok) { if (!response?.ok) {
setStatus(response?.error || "No text detected."); setStatus(response?.error || "No text detected.");
return false; return false;
} }
state.siteText = response.extracted || ""; state.siteText = response.extracted || "";
state.siteTextSelector = response.selector || selector;
updateSiteTextCount(); updateSiteTextCount();
updatePromptCount(0); updatePromptCount(0);
setStatus("Text extracted."); setStatus("Text extracted.");
@@ -1038,6 +1060,7 @@ partialTextPaste.addEventListener("input", async () => {
const response = await sendToActiveTab({ type: "FIND_SCOPE", text }); const response = await sendToActiveTab({ type: "FIND_SCOPE", text });
if (response?.ok) { if (response?.ok) {
state.siteText = response.extracted; state.siteText = response.extracted;
state.siteTextSelector = response.selector || "";
extractedPreview.textContent = state.siteText; extractedPreview.textContent = state.siteText;
await fillSiteDefaultsFromTab(); await fillSiteDefaultsFromTab();
switchState("review"); switchState("review");
@@ -1055,6 +1078,7 @@ extractFullBtn.addEventListener("click", async () => {
const response = await sendToActiveTab({ type: "EXTRACT_FULL" }); const response = await sendToActiveTab({ type: "EXTRACT_FULL" });
if (response?.ok) { if (response?.ok) {
state.siteText = response.extracted; state.siteText = response.extracted;
state.siteTextSelector = response.selector || "body";
extractedPreview.textContent = state.siteText; extractedPreview.textContent = state.siteText;
await fillSiteDefaultsFromTab(); await fillSiteDefaultsFromTab();
switchState("review"); switchState("review");
@@ -1083,6 +1107,7 @@ retryExtractBtn.addEventListener("click", () => {
urlPatternInput.value = ""; urlPatternInput.value = "";
siteNameInput.value = ""; siteNameInput.value = "";
state.siteText = ""; state.siteText = "";
state.siteTextSelector = "";
void clearPopupDraft(); void clearPopupDraft();
setStatus("Ready."); setStatus("Ready.");
}); });
@@ -1110,7 +1135,8 @@ confirmSiteBtn.addEventListener("click", async () => {
id: `site-${Date.now()}`, id: `site-${Date.now()}`,
name, name,
urlPattern: pattern, urlPattern: pattern,
workspaceId: "global" // Default to global for now workspaceId: "global", // Default to global for now
extractSelector: state.siteTextSelector || "body"
}; };
state.sites.push(newSite); state.sites.push(newSite);

View File

@@ -2480,6 +2480,7 @@ function collectSites() {
const nameInput = card.querySelector(".site-name"); const nameInput = card.querySelector(".site-name");
const patternInput = card.querySelector(".site-pattern"); const patternInput = card.querySelector(".site-pattern");
const workspaceSelect = card.querySelector(".site-workspace"); const workspaceSelect = card.querySelector(".site-workspace");
const extractInput = card.querySelector(".site-extract-selector");
const themeSelect = card.querySelector(".appearance-theme"); const themeSelect = card.querySelector(".appearance-theme");
const toolbarSelect = card.querySelector(".appearance-toolbar-position"); const toolbarSelect = card.querySelector(".appearance-toolbar-position");
const envsContainer = card.querySelector(".site-envs"); const envsContainer = card.querySelector(".site-envs");
@@ -2496,6 +2497,7 @@ function collectSites() {
name: (nameInput?.value || "").trim(), name: (nameInput?.value || "").trim(),
urlPattern: (patternInput?.value || "").trim(), urlPattern: (patternInput?.value || "").trim(),
workspaceId: workspaceSelect?.value || "global", workspaceId: workspaceSelect?.value || "global",
extractSelector: (extractInput?.value || "").trim(),
theme: themeSelect?.value || "inherit", theme: themeSelect?.value || "inherit",
toolbarPosition: toolbarSelect?.value || "inherit", toolbarPosition: toolbarSelect?.value || "inherit",
envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [], envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [],
@@ -2611,6 +2613,22 @@ function buildSiteCard(site, allWorkspaces = []) {
row.appendChild(deleteBtn); row.appendChild(deleteBtn);
card.appendChild(row); card.appendChild(row);
const extractField = document.createElement("div");
extractField.className = "field";
const extractLabel = document.createElement("label");
extractLabel.textContent = "Site Text Selector";
const extractInput = document.createElement("input");
extractInput.type = "text";
extractInput.value = site.extractSelector || "";
extractInput.className = "site-extract-selector";
extractInput.placeholder = "body";
extractInput.addEventListener("input", () => {
scheduleSidebarErrors();
});
extractField.appendChild(extractLabel);
extractField.appendChild(extractInput);
card.appendChild(extractField);
const appearanceSection = buildAppearanceSection({ const appearanceSection = buildAppearanceSection({
theme: site.theme || "inherit", theme: site.theme || "inherit",
toolbarPosition: site.toolbarPosition || "inherit" toolbarPosition: site.toolbarPosition || "inherit"
@@ -3396,6 +3414,7 @@ async function loadSettings() {
...site, ...site,
name: site.name || site.urlPattern || "", name: site.name || site.urlPattern || "",
workspaceId: site.workspaceId || "global", workspaceId: site.workspaceId || "global",
extractSelector: typeof site.extractSelector === "string" ? site.extractSelector : "",
theme: site.theme || "inherit", theme: site.theme || "inherit",
toolbarPosition: site.toolbarPosition || "inherit", toolbarPosition: site.toolbarPosition || "inherit",
envConfigs: normalizeConfigList(site.envConfigs), envConfigs: normalizeConfigList(site.envConfigs),