// Shared podcast-RSS parsing, used by both the main show (episodes.js) and // any sub-show fed by its own feed URL (nuggets.js). export function extractCdata(raw) { const cdata = /^$/.exec(raw.trim()) return cdata ? cdata[1].trim() : raw.trim() } function decodeEntities(str) { return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/'/g, "'") .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n))) .replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCharCode(parseInt(h, 16))) .replace(/ /g, ' ') } export function parseRssItems(xml, limit = Infinity) { const items = [] const itemRegex = /([\s\S]*?)<\/item>/g let match while ((match = itemRegex.exec(xml)) !== null && items.length < limit) { const block = match[1] const titleRaw = /([\s\S]*?)<\/title>/.exec(block)?.[1] ?? '' const title = extractCdata(titleRaw) if (!title) continue const pubDate = (/<pubDate>([\s\S]*?)<\/pubDate>/.exec(block)?.[1] ?? '').trim() const guidRaw = /<guid[^>]*>([\s\S]*?)<\/guid>/.exec(block)?.[1] ?? '' const guid = extractCdata(guidRaw) const enclosureUrl = /<enclosure[^>]+url="([^"]+)"/.exec(block)?.[1] ?? '' const link = guid.startsWith('http') ? guid : enclosureUrl const descRaw = /<description>([\s\S]*?)<\/description>/.exec(block)?.[1] ?? '' const descText = decodeEntities(extractCdata(descRaw).replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim() const duration = (/<itunes:duration>([\s\S]*?)<\/itunes:duration>/.exec(block)?.[1] ?? '').trim() const episode = (/<itunes:episode>([\s\S]*?)<\/itunes:episode>/.exec(block)?.[1] ?? '').trim() const season = (/<itunes:season>([\s\S]*?)<\/itunes:season>/.exec(block)?.[1] ?? '').trim() items.push({ title, pubDate, link, audioUrl: enclosureUrl, description: descText.slice(0, 220) + (descText.length > 220 ? '…' : ''), duration, episode, season, }) } return items }