From 488138a38985e94fecc8be13166d1b99619d2e9b Mon Sep 17 00:00:00 2001 From: Peisong Xiao Date: Sun, 18 Jan 2026 09:29:24 -0500 Subject: [PATCH] fortified selector logic, [bug] selector can't process complex queries --- sitecompanion/content.js | 67 +++++++++++++++++++++++++++++++++++-- sitecompanion/manifest.json | 2 +- sitecompanion/popup.js | 38 +++++++++++++++++---- sitecompanion/settings.js | 19 +++++++++++ 4 files changed, 117 insertions(+), 9 deletions(-) diff --git a/sitecompanion/content.js b/sitecompanion/content.js index 1a7643f..cc5152f 100644 --- a/sitecompanion/content.js +++ b/sitecompanion/content.js @@ -22,6 +22,45 @@ function findMinimumScope(text) { 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) { return (value || "").trim().toLowerCase(); } @@ -274,12 +313,36 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { sendResponse({ ok: false, error: "Scope not found." }); 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; } if (message.type === "EXTRACT_FULL") { const extracted = document.body?.innerText || ""; - sendResponse({ ok: true, extracted }); + sendResponse({ ok: true, extracted, selector: "body" }); } }); diff --git a/sitecompanion/manifest.json b/sitecompanion/manifest.json index 59922fb..1f7d7b0 100644 --- a/sitecompanion/manifest.json +++ b/sitecompanion/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "SiteCompanion", - "version": "0.4.0", + "version": "0.4.1", "description": "AI companion for site-bound text extraction and tasks.", "permissions": ["storage", "activeTab"], "host_permissions": [""], diff --git a/sitecompanion/popup.js b/sitecompanion/popup.js index ed8420d..5f79528 100644 --- a/sitecompanion/popup.js +++ b/sitecompanion/popup.js @@ -49,6 +49,7 @@ const state = { currentPopupState: "unknown", globalTheme: "system", forcedTask: null, + siteTextSelector: "", selectedTaskId: "", selectedEnvId: "", selectedProfileId: "" @@ -75,7 +76,8 @@ function buildPopupDraft() { state: state.currentPopupState, siteText: state.siteText || "", 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") { siteNameInput.value = draft.siteName; } + if (typeof draft.siteTextSelector === "string") { + state.siteTextSelector = draft.siteTextSelector; + } } function matchUrl(url, pattern) { @@ -190,10 +195,14 @@ function filterApiConfigsForScope(apiConfigs, workspace, site) { async function detectSite(url) { 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; - const site = sites.find(s => matchUrl(url, s.urlPattern)); + const site = normalizedSites.find((s) => matchUrl(url, s.urlPattern)); if (site) { state.currentSite = site; const workspace = @@ -695,7 +704,12 @@ async function loadConfig() { const envs = normalizeConfigList(stored.envConfigs); const profiles = normalizeConfigList(stored.profiles); 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) ? stored.workspaces : state.workspaces; @@ -709,6 +723,9 @@ async function loadConfig() { activeSite && activeSite.workspaceId ? workspaces.find((entry) => entry.id === activeSite.workspaceId) : null; + if (activeSite) { + state.currentSite = activeSite; + } if (activeWorkspace) { state.currentWorkspace = activeWorkspace; currentWorkspaceName.textContent = activeWorkspace.name || "Global"; @@ -795,13 +812,18 @@ async function loadTheme() { async function handleExtract() { setStatus("Extracting..."); 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) { setStatus(response?.error || "No text detected."); return false; } state.siteText = response.extracted || ""; + state.siteTextSelector = response.selector || selector; updateSiteTextCount(); updatePromptCount(0); setStatus("Text extracted."); @@ -1038,6 +1060,7 @@ partialTextPaste.addEventListener("input", async () => { const response = await sendToActiveTab({ type: "FIND_SCOPE", text }); if (response?.ok) { state.siteText = response.extracted; + state.siteTextSelector = response.selector || ""; extractedPreview.textContent = state.siteText; await fillSiteDefaultsFromTab(); switchState("review"); @@ -1055,6 +1078,7 @@ extractFullBtn.addEventListener("click", async () => { const response = await sendToActiveTab({ type: "EXTRACT_FULL" }); if (response?.ok) { state.siteText = response.extracted; + state.siteTextSelector = response.selector || "body"; extractedPreview.textContent = state.siteText; await fillSiteDefaultsFromTab(); switchState("review"); @@ -1083,6 +1107,7 @@ retryExtractBtn.addEventListener("click", () => { urlPatternInput.value = ""; siteNameInput.value = ""; state.siteText = ""; + state.siteTextSelector = ""; void clearPopupDraft(); setStatus("Ready."); }); @@ -1110,7 +1135,8 @@ confirmSiteBtn.addEventListener("click", async () => { id: `site-${Date.now()}`, name, urlPattern: pattern, - workspaceId: "global" // Default to global for now + workspaceId: "global", // Default to global for now + extractSelector: state.siteTextSelector || "body" }; state.sites.push(newSite); diff --git a/sitecompanion/settings.js b/sitecompanion/settings.js index 16d29c1..154b7d9 100644 --- a/sitecompanion/settings.js +++ b/sitecompanion/settings.js @@ -2480,6 +2480,7 @@ function collectSites() { const nameInput = card.querySelector(".site-name"); const patternInput = card.querySelector(".site-pattern"); const workspaceSelect = card.querySelector(".site-workspace"); + const extractInput = card.querySelector(".site-extract-selector"); const themeSelect = card.querySelector(".appearance-theme"); const toolbarSelect = card.querySelector(".appearance-toolbar-position"); const envsContainer = card.querySelector(".site-envs"); @@ -2496,6 +2497,7 @@ function collectSites() { name: (nameInput?.value || "").trim(), urlPattern: (patternInput?.value || "").trim(), workspaceId: workspaceSelect?.value || "global", + extractSelector: (extractInput?.value || "").trim(), theme: themeSelect?.value || "inherit", toolbarPosition: toolbarSelect?.value || "inherit", envConfigs: envsContainer ? collectEnvConfigs(envsContainer) : [], @@ -2611,6 +2613,22 @@ function buildSiteCard(site, allWorkspaces = []) { row.appendChild(deleteBtn); 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({ theme: site.theme || "inherit", toolbarPosition: site.toolbarPosition || "inherit" @@ -3396,6 +3414,7 @@ async function loadSettings() { ...site, name: site.name || site.urlPattern || "", workspaceId: site.workspaceId || "global", + extractSelector: typeof site.extractSelector === "string" ? site.extractSelector : "", theme: site.theme || "inherit", toolbarPosition: site.toolbarPosition || "inherit", envConfigs: normalizeConfigList(site.envConfigs),