summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-08-03 22:09:48 +0100
committerIgor Soarez <igor@soarez.org>2026-08-03 22:09:48 +0100
commit7a8f261ec50b9c15bc37ba8ae1f0022c392111d3 (patch)
tree2157aaa360c41577910267fb513a9980920cc743
parent97b7804d02d0a8ffc9afdc64069069baf93ef392 (diff)
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.
-rw-r--r--src/engines.ts8
-rw-r--r--src/extract.ts45
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;