import { load } from "cheerio";
// ─── HTML Extraction ─────────────────────────────────────────────────────────
/**
* Extract readable text from HTML using Cheerio.
* Removes scripts, styles, nav, ads, and other non-content elements,
* then tries to find the main content area (article, main, .content, etc.)
* before falling back to the full body text.
*/
export function stripHtml(html: string): string {
const $ = load(html);
// Remove non-content elements
$("script, style, noscript, template, iframe, svg").remove();
$("nav, footer, aside, header, menu, button, form").remove();
$("[role='navigation'], [role='banner'], [role='complementary']").remove();
$(".advertisement, .ad, .ads, .cookie-banner, .newsletter").remove();
// Try to find the main content area
let $content = $("article, main, [role='main'], .content, #content, .post, .entry, .article").first();
// Fallback to body or html
if (!$content.length) {
$content = $("body, html").first();
}
// Get text — cheerio decodes HTML entities automatically
let text = $content.text();
// Normalize whitespace
text = text
.replace(/\u00A0/g, " ") // non-breaking space → regular space
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n")
.replace(/[ \t]*\n[ \t]*/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.replace(/[ \t]+/g, " ")
.trim();
return text;
}
export function normalizeUrl(url: string): string {
try {
const u = new URL(url);
u.hostname = u.hostname.toLowerCase();
u.hash = "";
let path = u.pathname;
if (path.endsWith("/") && path.length > 1) {
path = path.slice(0, -1);
}
u.pathname = path;
return u.toString();
} catch {
return url;
}
}
export function isInternalUrl(url: string): boolean {
try {
const u = new URL(url);
let hostname = u.hostname.toLowerCase();
// Strip IPv6 brackets so "[::1]" → "::1"
if (hostname.startsWith("[") && hostname.endsWith("]")) {
hostname = hostname.slice(1, -1);
}
if (hostname === "localhost" || hostname === "0.0.0.0" || hostname === "::1" || hostname === "::") return true;
if (hostname.startsWith("127.")) return true;
if (hostname.startsWith("10.")) return true;
if (hostname.startsWith("192.168.")) return true;
if (hostname.startsWith("172.")) {
const parts = hostname.split(".");
const second = parseInt(parts[1], 10);
if (second >= 16 && second <= 31) return true;
}
if (hostname.startsWith("169.254.")) return true;
if (hostname.startsWith("fc") || hostname.startsWith("fd")) return true;
if (u.protocol === "file:") return true;
return false;
} catch {
return false;
}
}
export function urlContainsSensitiveParam(url: string): boolean {
try {
const u = new URL(url);
const sensitive = ["token", "key", "api_key", "apikey", "secret", "password", "auth", "credential"];
for (const param of sensitive) {
if (u.searchParams.has(param)) {
const value = u.searchParams.get(param) ?? "";
if (value.length > 4) return true;
}
}
return false;
} catch {
return false;
}
}