// author: simon-pierre boucher import { Readability } from "@mozilla/readability"; import { parse, stripChrome } from "./dom.js"; const MIN_READABILITY_CHARS = 200; function textLen(el: Element): number { return (el.textContent ?? "").replace(/\s+/g, " ").trim().length; } function linkTextLen(el: Element): number { let sum = 0; for (const a of Array.from(el.querySelectorAll("a"))) sum += textLen(a); return sum; } /** * Density-based main-content fallback (§8.2). For each candidate block, score it * by `textLength^2 / (1 + linkTextLength)` — favouring large, low-link subtrees — * and return the winner's HTML. This is the safety net for listing/product pages * where Readability silently returns almost nothing. */ export function densityExtract(document: Document): string { const candidates = Array.from(document.querySelectorAll("article, main, section, div, td")); let best: Element | null = document.body; let bestScore = -1; for (const el of candidates) { const t = textLen(el); if (t < 100) continue; const score = (t * t) / (1 + linkTextLen(el)); if (score > bestScore) { bestScore = score; best = el; } } return (best ?? document.body).innerHTML; } export interface MainContent { readonly html: string; readonly usedReadability: boolean; } export function extractMainContent(html: string, onlyMainContent: boolean): MainContent { if (!onlyMainContent) { const { document } = parse(html); return { html: document.body.innerHTML, usedReadability: false }; } try { const { document } = parse(html); const article = new Readability(document as unknown as Document, { charThreshold: MIN_READABILITY_CHARS }).parse(); if (article && (article.textContent ?? "").trim().length >= MIN_READABILITY_CHARS && article.content) { return { html: article.content, usedReadability: true }; } } catch { /* fall through to density */ } const { document } = parse(html); stripChrome(document); return { html: densityExtract(document), usedReadability: false }; }