camoufox-mcp-server
Version:
MCP server for browser automation using Camoufox - a privacy-focused Firefox fork with advanced anti-detection features
112 lines (111 loc) • 4.45 kB
JavaScript
import { redactUrl } from "../utils.js";
export async function buildFindPayload(page, response, query, maxMatches, contextChars, selector) {
const extracted = await page.evaluate(({ searchQuery, maxItems, surroundingChars, cssSelector }) => {
const root = cssSelector
? document.querySelector(cssSelector)
: document.body ?? document.documentElement;
if (!root) {
return { matches: [], truncated: false, found: false };
}
function cssIdent(value) {
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
return CSS.escape(value);
}
return value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
}
function selectorFor(element) {
if (element.id) {
return `#${cssIdent(element.id)}`;
}
const path = [];
let current = element;
while (current && current !== document.documentElement && path.length < 8) {
let part = current.tagName.toLowerCase();
const parent = current.parentElement;
if (parent) {
const currentTagName = current.tagName;
const sameTagSiblings = Array.from(parent.children).filter((child) => child.tagName === currentTagName);
if (sameTagSiblings.length > 1) {
part += `:nth-of-type(${sameTagSiblings.indexOf(current) + 1})`;
}
}
path.unshift(part);
current = parent;
}
return path.join(" > ");
}
function isHiddenElement(element) {
if (["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT"].includes(element.tagName)) {
return true;
}
if (element instanceof HTMLElement && element.hidden) {
return true;
}
if (element.getAttribute("aria-hidden") === "true") {
return true;
}
const style = window.getComputedStyle(element);
return style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse";
}
function isTextNodeVisible(node) {
let current = node.parentElement;
while (current) {
if (isHiddenElement(current)) {
return false;
}
if (current === root) {
return true;
}
current = current.parentElement;
}
return true;
}
const normalizedQuery = searchQuery.toLowerCase();
const matches = [];
let truncated = false;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
if (!node.nodeValue || !isTextNodeVisible(node)) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
},
});
while (true) {
const node = walker.nextNode();
if (!node) {
break;
}
const rawText = (node.nodeValue ?? "").replace(/\s+/g, " ");
const index = rawText.toLowerCase().indexOf(normalizedQuery);
if (index < 0) {
continue;
}
if (matches.length >= maxItems) {
truncated = true;
break;
}
const start = Math.max(0, index - surroundingChars);
const end = Math.min(rawText.length, index + searchQuery.length + surroundingChars);
matches.push({
text: rawText.slice(start, end).trim(),
selector: selectorFor(node.parentElement ?? root),
score: 1,
});
}
return { matches, truncated, found: true };
}, { searchQuery: query, maxItems: maxMatches, surroundingChars: contextChars, cssSelector: selector });
return {
url: redactUrl(page.url()),
title: await page.title(),
status: response?.status(),
contentType: response?.headers()["content-type"],
query,
selector,
selectorFound: extracted.found,
matches: extracted.matches,
truncated: extracted.truncated,
maxMatches,
contextChars,
};
}