fortified selector logic, [bug] selector can't process complex queries
This commit is contained in:
@@ -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" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>"],
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user