From 7a8f261ec50b9c15bc37ba8ae1f0022c392111d3 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Mon, 3 Aug 2026 22:09:48 +0100 Subject: Take Brave snippets by position rather than by class name Brave's source name was leaking into every snippet ("Medium March 27, 2025 - ..."), because the .sitename/.netloc selectors guessed for it match nothing. Rather than guess again, use the ordering: an engine rendering "source / breadcrumb / title / description" puts all its metadata before the title, so everything after the title line is the description. Class names churn; that ordering does not. The named selectors stay as the fallback for when the title is not on a line of its own. Verified against Brave, and Google/DuckDuckGo/Bing re-checked for regressions. --- src/engines.ts | 8 +++++--- src/extract.ts | 45 ++++++++++++++++++++++++++++++++------------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/engines.ts b/src/engines.ts index 79462fc..a71b9c8 100644 --- a/src/engines.ts +++ b/src/engines.ts @@ -190,9 +190,11 @@ const BRAVE: EngineDefinition = { item: '.snippet[data-type="web"]', link: "a[href]", title: ".title", - // No dedicated description element; the item's text is - // "source | breadcrumb | title | description", so subtract the first three. - subtract: [".title", "cite", ".sitename", ".netloc"], + // No dedicated description element. The item's text is + // "source | breadcrumb | title | description", so the extractor keeps only + // the lines after the title. These selectors are the fallback for when the + // title is not on a line of its own. + subtract: [".title", "cite"], unwrap: "none", selfHostPattern: String.raw`^https?://(search\.)?brave\.com/`, }, diff --git a/src/extract.ts b/src/extract.ts index 282fa07..8b5c57f 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -177,21 +177,40 @@ export const buildProbeScript = (config: ExtractionConfig): string => String.raw if (snippetEl) { snippet = norm(snippetEl.innerText); } else { - // No dedicated description element: subtract the parts we can name - // (title, breadcrumb, source) and keep what is left. - var drop = {}; - for (var s = 0; CFG.subtract && s < CFG.subtract.length; s++) { - var parts = item.querySelectorAll(CFG.subtract[s]); - for (var p = 0; p < parts.length; p++) { - var pl = linesOf(parts[p]); - for (var q = 0; q < pl.length; q++) drop[norm(pl[q])] = true; + // No dedicated description element. Rather than guess at class names + // for the source and breadcrumb, use their position: an engine that + // renders "source / breadcrumb / title / description" puts every piece + // of metadata *before* the title, so everything after the title line is + // the description. Class names churn; that ordering does not. + var itemLines = linesOf(item); + var titleIndex = -1; + for (var t = 0; t < itemLines.length; t++) { + if (norm(itemLines[t]) === title) { titleIndex = t; break; } + } + + var candidate; + if (titleIndex >= 0) { + candidate = itemLines.slice(titleIndex + 1); + } else { + // Title is not its own line (it may be inline with other text). + // Fall back to subtracting the parts we can name. + var drop = {}; + for (var s = 0; CFG.subtract && s < CFG.subtract.length; s++) { + var parts = item.querySelectorAll(CFG.subtract[s]); + for (var p = 0; p < parts.length; p++) { + var pl = linesOf(parts[p]); + for (var q = 0; q < pl.length; q++) drop[norm(pl[q])] = true; + } } + candidate = itemLines.filter(function (l) { return !drop[norm(l)]; }); } - var kept = linesOf(item).filter(function (l) { - var n = norm(l); - return !drop[n] && n !== title && !/^https?:\/\//.test(n) && n.indexOf("›") === -1; - }); - snippet = norm(kept.join(" ")); + + snippet = norm( + candidate.filter(function (l) { + var n = norm(l); + return n !== title && !/^https?:\/\//.test(n) && n.indexOf("›") === -1; + }).join(" "), + ); } seen[url] = true; -- cgit v1.3.1