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; } }