@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
JavaScript
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)}`;
};