/** * HTML-to-text conversion for PodTUI * * Converts HTML content from RSS feed descriptions into clean plain text * suitable for display in the terminal. Preserves paragraph structure, * converts lists to bulleted text, and strips all tags. */ /** * Convert HTML content to readable plain text. * * - Block elements (

,

,
, headings,
  • ) become line breaks * -
  • items get a bullet prefix * - text becomes "text (url)" * - All other tags are stripped * - HTML entities are decoded * - Excessive whitespace is collapsed */ export function htmlToText(html: string): string { if (!html) return "" let text = html // Strip CDATA wrappers text = text.replace(//gi, "$1") // Replace
    /
    with newline text = text.replace(//gi, "\n") // Replace
    with a separator line text = text.replace(//gi, "\n---\n") // Block-level elements get newlines before/after text = text.replace(/<\/?(p|div|blockquote|pre|h[1-6]|table|tr|section|article|header|footer)[\s>][^>]*>/gi, "\n") // List items get bullet prefix text = text.replace(/]*>/gi, "\n - ") text = text.replace(/<\/li>/gi, "") // Strip list wrappers text = text.replace(/<\/?(ul|ol|dl|dt|dd)[^>]*>/gi, "\n") // Convert links: text -> text (url) text = text.replace(/]*href=["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, linkText) => { const cleanText = stripTags(linkText).trim() if (!cleanText) return href // Don't duplicate if the link text IS the URL if (cleanText === href || cleanText === href.replace(/^https?:\/\//, "")) return cleanText return `${cleanText} (${href})` }) // Strip all remaining tags text = stripTags(text) // Decode HTML entities text = decodeHtmlEntities(text) // Collapse multiple blank lines into at most two newlines text = text.replace(/\n{3,}/g, "\n\n") // Collapse runs of spaces/tabs (but not newlines) on each line text = text .split("\n") .map((line) => line.replace(/[ \t]+/g, " ").trim()) .join("\n") return text.trim() } /** Strip all HTML/XML tags from a string */ function stripTags(html: string): string { return html.replace(/<[^>]*>/g, "") } /** Decode common HTML entities */ function decodeHtmlEntities(text: string): string { return text // Named entities .replace(/</g, "<") .replace(/>/g, ">") .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(/ /g, " ") .replace(/—/g, "\u2014") .replace(/–/g, "\u2013") .replace(/…/g, "\u2026") .replace(/«/g, "\u00AB") .replace(/»/g, "\u00BB") .replace(/“/g, "\u201C") .replace(/”/g, "\u201D") .replace(/‘/g, "\u2018") .replace(/’/g, "\u2019") .replace(/•/g, "\u2022") .replace(/©/g, "\u00A9") .replace(/®/g, "\u00AE") .replace(/™/g, "\u2122") .replace(/°/g, "\u00B0") .replace(/×/g, "\u00D7") // Numeric entities (decimal) .replace(/&#(\d+);/g, (_, code) => { const n = parseInt(code, 10) return n > 0 && n < 0x10ffff ? String.fromCodePoint(n) : "" }) // Numeric entities (hex) .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => { const n = parseInt(hex, 16) return n > 0 && n < 0x10ffff ? String.fromCodePoint(n) : "" }) }