UNPKG

@bliztek/feed-generator

Version:

A simple and lightweight Node.js library for generating RSS 2.0, Atom, and JSON Feed formats.

204 lines (203 loc) 10 kB
import { xmlDeclaration, renderXml, el, nodes, escapeXml } from "../xml.js"; const toRfc822 = (date) => new Date(date).toUTCString(); const formatDuration = (duration) => { if (typeof duration === "string") return duration; const h = Math.floor(duration / 3600); const m = Math.floor((duration % 3600) / 60); const s = duration % 60; return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}` : `${m}:${String(s).padStart(2, "0")}`; }; const renderPodcastCategories = (category) => { if (!category) return []; if (typeof category === "string") { return [{ tag: "itunes:category", selfClosing: true, attributes: { text: category } }]; } return category.map((cat) => { if (cat.subcategory) { return { tag: "itunes:category", attributes: { text: cat.name }, children: [ { tag: "itunes:category", selfClosing: true, attributes: { text: cat.subcategory } }, ], }; } return { tag: "itunes:category", selfClosing: true, attributes: { text: cat.name } }; }); }; export const renderRSS = (feed, options = { compact: false }) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; const { compact, stylesheet } = options; const selfLink = (_b = (_a = feed.feedLinks) === null || _a === void 0 ? void 0 : _a.atom) !== null && _b !== void 0 ? _b : `${feed.link}/feed.xml`; const hasPodcast = !!feed.podcast; const imageNode = feed.image ? { tag: "image", children: nodes(el("url", feed.image.url), el("title", (_c = feed.image.title) !== null && _c !== void 0 ? _c : feed.title), el("link", (_d = feed.image.link) !== null && _d !== void 0 ? _d : feed.link), feed.image.width ? el("width", String(feed.image.width)) : null, feed.image.height ? el("height", String(feed.image.height)) : null), } : null; const categoryNodes = ((_e = feed.categories) !== null && _e !== void 0 ? _e : []).map((cat) => ({ tag: "category", attributes: cat.domain ? { domain: cat.domain } : undefined, children: [cat.name], })); // Podcast channel-level tags const podcastChannelNodes = []; if (feed.podcast) { const pc = feed.podcast; if (pc.type) podcastChannelNodes.push(el("itunes:type", pc.type)); if (pc.explicit !== undefined) podcastChannelNodes.push(el("itunes:explicit", pc.explicit ? "true" : "false")); if (pc.image) podcastChannelNodes.push({ tag: "itunes:image", selfClosing: true, attributes: { href: pc.image }, }); if (pc.owner) { podcastChannelNodes.push({ tag: "itunes:owner", children: nodes(el("itunes:name", pc.owner.name), el("itunes:email", pc.owner.email)), }); } podcastChannelNodes.push(...renderPodcastCategories(pc.category)); if (pc.newFeedUrl) podcastChannelNodes.push(el("itunes:new-feed-url", pc.newFeedUrl)); if (pc.block) podcastChannelNodes.push(el("itunes:block", "Yes")); if (pc.complete) podcastChannelNodes.push(el("itunes:complete", "Yes")); if ((_g = (_f = feed.authors) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.name) podcastChannelNodes.push(el("itunes:author", feed.authors[0].name)); } const itemNodes = feed.items.map((item) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; const itemCategories = ((_a = item.categories) !== null && _a !== void 0 ? _a : []).map((cat) => ({ tag: "category", attributes: cat.domain ? { domain: cat.domain } : undefined, children: [cat.name], })); const enclosure = ((_b = item.enclosures) === null || _b === void 0 ? void 0 : _b[0]) ? { tag: "enclosure", selfClosing: true, attributes: { url: item.enclosures[0].url, type: item.enclosures[0].type, length: item.enclosures[0].length, }, } : null; const pubDate = (_d = (_c = item.date) !== null && _c !== void 0 ? _c : feed.updated) !== null && _d !== void 0 ? _d : new Date(); // Podcast item-level tags const podcastItemNodes = []; if (item.podcast) { const pi = item.podcast; if (pi.duration !== undefined) podcastItemNodes.push(el("itunes:duration", formatDuration(pi.duration))); if (pi.explicit !== undefined) podcastItemNodes.push(el("itunes:explicit", pi.explicit ? "true" : "false")); if (pi.episode !== undefined) podcastItemNodes.push(el("itunes:episode", String(pi.episode))); if (pi.season !== undefined) podcastItemNodes.push(el("itunes:season", String(pi.season))); if (pi.episodeType) podcastItemNodes.push(el("itunes:episodeType", pi.episodeType)); if (pi.image) podcastItemNodes.push({ tag: "itunes:image", selfClosing: true, attributes: { href: pi.image }, }); if (pi.block) podcastItemNodes.push(el("itunes:block", "Yes")); if ((_f = (_e = item.authors) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.name) podcastItemNodes.push(el("itunes:author", item.authors[0].name)); } return { tag: "item", children: nodes(el("title", (_g = item.title) !== null && _g !== void 0 ? _g : "Untitled"), el("link", item.link), { tag: "guid", attributes: { isPermaLink: item.link ? "true" : "false" }, children: [item.id], }, item.content ? el("description", item.content, { cdata: true }) : item.summary ? el("description", item.summary, { cdata: true }) : null, item.content ? el("content:encoded", item.content, { cdata: true }) : null, enclosure, item.image ? { tag: "media:content", selfClosing: true, attributes: { url: item.image, medium: "image" }, } : null, el("pubDate", toRfc822(pubDate)), ...((_h = item.authors) !== null && _h !== void 0 ? _h : []).filter((a) => a.email).map((a) => el("author", a.name ? `${a.email} (${a.name})` : a.email)), ...((_j = item.authors) !== null && _j !== void 0 ? _j : []).filter((a) => !a.email && a.name).map((a) => el("dc:creator", a.name)), ...itemCategories, ...podcastItemNodes, ...((_l = (_k = item.extensions) === null || _k === void 0 ? void 0 : _k.xmlElements) !== null && _l !== void 0 ? _l : [])), }; }); const channel = { tag: "channel", children: nodes(el("title", feed.title), el("link", feed.link), el("description", feed.description), el("language", feed.language), imageNode, el("copyright", feed.copyright), feed.updated ? el("lastBuildDate", toRfc822(feed.updated)) : null, el("generator", (_h = feed.generator) !== null && _h !== void 0 ? _h : "Bliztek Feed Generator"), feed.ttl ? el("ttl", String(feed.ttl)) : null, ((_j = feed.skipHours) === null || _j === void 0 ? void 0 : _j.length) ? { tag: "skipHours", children: feed.skipHours.map((h) => ({ tag: "hour", children: [String(h)] })), } : null, ((_k = feed.skipDays) === null || _k === void 0 ? void 0 : _k.length) ? { tag: "skipDays", children: feed.skipDays.map((d) => ({ tag: "day", children: [d] })), } : null, ...categoryNodes, ...podcastChannelNodes, { tag: "atom:link", selfClosing: true, attributes: { href: selfLink, rel: "self", type: "application/rss+xml", }, }, feed.nextUrl ? { tag: "atom:link", selfClosing: true, attributes: { href: feed.nextUrl, rel: "next" }, } : null, ((_l = feed.feedLinks) === null || _l === void 0 ? void 0 : _l.hub) ? { tag: "atom:link", selfClosing: true, attributes: { href: feed.feedLinks.hub, rel: "hub" }, } : null, ...((_o = (_m = feed.extensions) === null || _m === void 0 ? void 0 : _m.xmlElements) !== null && _o !== void 0 ? _o : []), ...itemNodes), }; const rssAttrs = { version: "2.0", "xmlns:atom": "http://www.w3.org/2005/Atom", "xmlns:content": "http://purl.org/rss/1.0/modules/content/", "xmlns:dc": "http://purl.org/dc/elements/1.1/", }; const hasMedia = feed.items.some((item) => !!item.image); if (hasMedia) { rssAttrs["xmlns:media"] = "http://search.yahoo.com/mrss/"; } if (hasPodcast) { rssAttrs["xmlns:itunes"] = "http://www.itunes.com/dtds/podcast-1.0.dtd"; } if ((_p = feed.extensions) === null || _p === void 0 ? void 0 : _p.xmlNamespaces) { Object.assign(rssAttrs, feed.extensions.xmlNamespaces); } const rss = { tag: "rss", attributes: rssAttrs, children: [channel], }; const sep = compact ? "" : "\n"; const stylesheetPI = stylesheet ? `${sep}<?xml-stylesheet type="text/xsl" href="${escapeXml(stylesheet)}"?>` : ""; return `${xmlDeclaration()}${stylesheetPI}${sep}${renderXml([rss], 0, compact)}`; };