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