diff --git a/apps/extension/src/entrypoints/content/index.tsx b/apps/extension/src/entrypoints/content/index.tsx index 359a466..8c15fc4 100644 --- a/apps/extension/src/entrypoints/content/index.tsx +++ b/apps/extension/src/entrypoints/content/index.tsx @@ -33,7 +33,6 @@ function orderedResults(results: Results, rankings: RankingsV2 | null) { case "lower": return { ...result, ord: order - weight, rank }; case "block": - result.element.remove(); return null; // return { ...result, ord: order - 9999, rank }; default: @@ -76,8 +75,20 @@ function reorderResults( } function sortResults(results: Results, rankings: RankingsV2 | null) { - const rankedResults = orderedResults(results, rankings); + const visibleResults = results.filter((result) => { + if (rankings?.[result.domain]?.type === "block") { + result.element.remove(); + return false; + } + return true; + }); + + const reorderableResults = visibleResults.filter( + (result) => result.canReorder, + ); + const rankedResults = orderedResults(reorderableResults, rankings); reorderResults(rankedResults); + return visibleResults; } function addPopupContainers(searches: Results) { @@ -125,8 +136,8 @@ function main(config: { }) { if (!config.rankings_active) return; const searches = getResults(); - sortResults(searches, config.rankings); - addPopupContainers(searches); + const visibleSearches = sortResults(searches, config.rankings); + addPopupContainers(visibleSearches); } function runOnBody(condition: () => boolean, callback: () => void) { @@ -165,6 +176,9 @@ export default defineContentScript({ clearTimeout(timeout); configPromise .then((config) => main(config)) + .catch((error) => { + console.error("[SearchTuner] main failed", error); + }) .finally(() => showMain()); }, ); diff --git a/apps/extension/src/utils/filter.ts b/apps/extension/src/utils/filter.ts index 19f7009..f920cb2 100644 --- a/apps/extension/src/utils/filter.ts +++ b/apps/extension/src/utils/filter.ts @@ -1,47 +1,53 @@ import $ from "jquery"; import { err, ok } from "neverthrow"; -// for refrence see https://github.com/benbusby/whoogle-search/blob/e4cabe3e5b9aa55cc14f845bb3e194d83d46ed1c/app/filter.py -// https://github.com/searxng/searxng/blob/885d02c8c3a3ae54177eab81e672abe65a76acf5/searx/engines/google.py - -const ITEM_PINNED_RESULT_CLASS = "BYM4Nd"; -const JSCONTROLLER_RESULT = "SC7lYd"; - -const JSNAME_LINK_ID = "UWckNb"; - -// const BLOCK_TITLES: string[] = [ -// "People also ask", -// "Related searches", -// "Videos", -// "Top stories", -// "Images", -// "News", -// ]; - -// Extend jQuery with custom methods -// declare global { -// interface JQuery { -// log(): JQuery; -// } -// } - -// // Add the log method to jQuery -// $.fn.log = function (this: JQuery) { -// // Convert jQuery object to array and spread it for console.log -// console.log.apply(console, this as any); -// return this; -// }; +// Keep these in sync with uBlacklist's desktop Google web selectors. +// https://github.com/ublacklist/builtin/blob/master/serpinfo/google.yml +const RESULT_SELECTORS = [ + { + type: "result", + root: ".vt6azd:not(.g-blk), .Ww4FFb", + url: ":is(.yuRUbf, .xe8e1b) a", + title: "h3", + }, + { type: "card", root: ".vCUuC", url: "a", title: ".Yt787" }, + { + type: "card", + root: ".sHEJob", + url: 'a[href^="http"]', + title: ".OSrXXb", + }, + { + type: "card", + root: "[data-news-cluster-id]", + url: "a", + title: '[role="heading"][aria-level="3"]', + }, + { type: "card", root: ".eejeod", url: "a", title: "h3" }, + { + type: "card", + root: ".ivg-i:not(.my5z3d)", + url: ".EZAeBe", + title: ".OSrXXb", + }, + { + type: "card", + root: ".ivg-i.my5z3d", + url: ".LBcIee", + title: ".ddBkwd", + }, +] as const; + +const RESULT_ROOT_SELECTOR = RESULT_SELECTORS.map(({ root }) => root).join( + ", ", +); export function getResults() { - const searches = extractDomains() + return extractDomains() .map((s) => { - if (s.isErr() || s.value.elementType !== "result") { - return null; - } - return s.value; + return s.isOk() ? s.value : null; }) .filter((s) => s !== null); - return searches; } export type Results = ReturnType; @@ -50,15 +56,11 @@ function extractDomains() { // Get the main results container const $rso = $("div#rso"); if ($rso.length === 0) { - console.error("Could not find result container #rso"); return []; } - // Filter out unwanted sections const blocks = $rso - .find( - `[jscontroller="${JSCONTROLLER_RESULT}"], .${ITEM_PINNED_RESULT_CLASS}`, - ) + .find(RESULT_ROOT_SELECTOR) .map((_, element) => $(element)) .toArray(); @@ -68,51 +70,57 @@ function extractDomains() { } function parseBlock(element: JQuery) { - const href = element - .find("a") - .filter(`[jsname="${JSNAME_LINK_ID}"]`) - .map((_, element) => ({ - href: $(element).attr("href"), - text: $(element).find("h3").text(), - })) - .get(); - - const isResult = element.is(`[jscontroller="${JSCONTROLLER_RESULT}"]`); - const isPinnedResult = element.is(`.${ITEM_PINNED_RESULT_CLASS}`); - - if (isResult || isPinnedResult) { - if (href[0]?.href === undefined) { - return err({ - error: "could_not_parse_domain" as const, - element, - }); - } - return ok({ - domain: getHostnames(href[0].href), - text: href[0].text, - elementType: "result" as const, + const selectors = RESULT_SELECTORS.find(({ root }) => element.is(root)); + if (!selectors) { + return err({ + error: "could_not_parse_domain" as const, element, }); - } else if (href.length === 0) { - return ok({ - elementType: "empty" as const, - element, - }); - } else { - return ok({ - elementType: "special" as const, + } + + const link = element + .find(selectors.url) + .filter((_, link) => link.closest(RESULT_ROOT_SELECTOR) === element[0]) + .first(); + const href = link.attr("href"); + const hrefDomain = href ? getHostname(href) : null; + const cite = link.find("cite").first().text(); + const citeDomain = + selectors.type === "result" ? getCitedHostname(cite) : null; + const domain = hrefDomain ?? citeDomain; + + if (!domain) { + return err({ + error: "could_not_parse_domain" as const, element, }); } + + return ok({ + domain, + domainSource: hrefDomain ? ("href" as const) : ("cite" as const), + text: element.find(selectors.title).first().text(), + // Rich result cards are safe to block individually, but moving them would + // pull them out of their containing news, image, or video module. + canReorder: + selectors.type === "result" && + element.parents(RESULT_ROOT_SELECTOR).length === 0, + element, + }); } -function getHostnames(url: string) { +function getHostname(url: string) { try { - if (url.startsWith("/") || url.startsWith("#")) { - throw new Error("Invalid URL"); - } - return new URL(url).hostname; + return new URL(url).hostname || null; } catch { - throw new Error("Invalid URL"); + return null; } } + +function getCitedHostname(cite: string) { + const [origin] = cite.trim().split(/\s+[›·]\s+/); + if (!origin) return null; + + const hostname = getHostname(origin) ?? getHostname(`https://${origin}`); + return hostname?.includes(".") ? hostname : null; +}