import { XMLParser } from 'fast-xml-parser'; /** One feed item: title always present (non-empty), link where the feed provides one. */ export interface FeedItem { title: string; link?: string; } const parser = new XMLParser({ ignoreAttributes: false }); function asText(value: unknown): string | undefined { if (typeof value === 'string') return value.trim() || undefined; // element with attributes/CDATA parses to an object holding its text under #text const t = (value as Record)?.['#text']; if (typeof t === 'string' && t.trim()) return t.trim(); return undefined; } function asLink(value: unknown): string | undefined { const fromText = asText(value); if (fromText) return fromText; // RSS 2.0 url // Atom — the URL is an attribute const href = (value as Record)?.['@_href']; if (typeof href === 'string' && href.trim()) return href.trim(); return undefined; } function collect(value: unknown): Record[] { // a single item/entry parses to an object, several to an array if (!value) return []; const arr = Array.isArray(value) ? value : [value]; return arr.filter( (v): v is Record => !!v && typeof v === 'object', ); } /** * Parse RSS 2.0 (`/<link>`) or Atom (`<entry><title>`) XML into items with * non-empty titles, in document order (feeds are newest-first). Returns [] for malformed * input or feeds without usable items — that empty result IS the "not a valid feed" signal. */ export function parseFeed(xml: string): FeedItem[] { let doc: unknown; try { doc = parser.parse(xml); } catch { return []; // fast-xml-parser is lenient, but a hard failure means no items either } const root = doc as Record<string, any>; // coalesce the RAW values first — collect() always returns an array, so `collect(a) ?? collect(b)` could never fall through to b const rawItems = collect(root?.rss?.channel?.item ?? root?.feed?.entry); const items: FeedItem[] = []; for (const item of rawItems) { const title = asText(item?.title); if (!title) continue; // skip entries without a usable title const link = asLink(item.link); items.push(link ? { title, link } : { title }); } return items; }