UNPKG

feed

Version:

Feed is a RSS, Atom and JSON feed generator for Node.js, making content syndication simple and intuitive!

500 lines (493 loc) 14.8 kB
import * as convert$1 from "xml-js"; import * as convert from "xml-js"; //#region src/config/index.ts const generator = "https://github.com/jpmonette/feed"; //#endregion //#region src/utils.ts function sanitize(url) { if (typeof url === "undefined") return; return url.replace(/&/g, "&"); } //#endregion //#region src/atom1.ts /** * Returns an Atom feed * @param ins */ var atom1_default = (ins) => { const { options } = ins; const base = { _declaration: { _attributes: { version: "1.0", encoding: "utf-8" } }, feed: { _attributes: { xmlns: "http://www.w3.org/2005/Atom" }, id: options.id, title: options.title, updated: options.updated ? options.updated.toISOString() : new Date().toISOString(), generator: sanitize(options.generator || generator) } }; if (options.author) base.feed.author = formatAuthor(options.author); base.feed.link = []; if (options.link) base.feed.link.push({ _attributes: { rel: "alternate", href: sanitize(options.link) } }); const atomLink = options.feed || options.feedLinks && options.feedLinks.atom; if (atomLink) base.feed.link.push({ _attributes: { rel: "self", href: sanitize(atomLink) } }); if (options.hub) base.feed.link.push({ _attributes: { rel: "hub", href: sanitize(options.hub) } }); /************************************************************************** * "feed" node: optional elements *************************************************************************/ if (options.description) base.feed.subtitle = options.description; if (options.image) base.feed.logo = options.image; if (options.favicon) base.feed.icon = options.favicon; if (options.copyright) base.feed.rights = options.copyright; base.feed.category = []; ins.categories.map((category) => { base.feed.category.push({ _attributes: { term: category } }); }); base.feed.contributor = []; ins.contributors.map((contributor) => { base.feed.contributor.push(formatAuthor(contributor)); }); base.feed.entry = []; /************************************************************************** * "entry" nodes *************************************************************************/ ins.items.map((item) => { const entry = { title: { _attributes: { type: "html" }, _cdata: item.title }, id: sanitize(item.id || item.link), link: [{ _attributes: { href: sanitize(item.link) } }], updated: item.date.toISOString() }; if (item.description) entry.summary = { _attributes: { type: "html" }, _cdata: item.description }; if (item.content) entry.content = { _attributes: { type: "html" }, _cdata: item.content }; if (Array.isArray(item.author)) { entry.author = []; item.author.map((author) => { entry.author.push(formatAuthor(author)); }); } if (Array.isArray(item.category)) { entry.category = []; item.category.map((category) => { entry.category.push(formatCategory$1(category)); }); } /** * Item Enclosure * https://validator.w3.org/feed/docs/atom.html#link */ if (item.enclosure) entry.link.push(formatEnclosure$1(item.enclosure)); if (item.image) entry.link.push(formatEnclosure$1(item.image, "image")); if (item.audio) entry.link.push(formatEnclosure$1(item.audio, "audio")); if (item.video) entry.link.push(formatEnclosure$1(item.video, "video")); if (item.contributor && Array.isArray(item.contributor)) { entry.contributor = []; item.contributor.map((contributor) => { entry.contributor.push(formatAuthor(contributor)); }); } if (item.published) entry.published = item.published.toISOString(); if (item.copyright) entry.rights = item.copyright; base.feed.entry.push(entry); }); return convert$1.js2xml(base, { compact: true, ignoreComment: true, spaces: 4 }); }; /** * Returns a formatted author * @param author */ const formatAuthor = (author) => { const { name, email, link } = author; const out = { name }; if (email) out.email = email; if (link) out.uri = sanitize(link); return out; }; /** * Returns a formated enclosure * @param enclosure * @param mimeCategory */ const formatEnclosure$1 = (enclosure, mimeCategory = "image") => { if (typeof enclosure === "string") { const type$1 = new URL(enclosure).pathname.split(".").slice(-1)[0]; return { _attributes: { rel: "enclosure", href: enclosure, type: `${mimeCategory}/${type$1}` } }; } const type = new URL(enclosure.url).pathname.split(".").slice(-1)[0]; return { _attributes: { rel: "enclosure", href: enclosure.url, title: enclosure.title, type: `${mimeCategory}/${type}`, length: enclosure.length } }; }; /** * Returns a formatted category * @param category */ const formatCategory$1 = (category) => { const { name, scheme, term } = category; return { _attributes: { label: name, scheme, term } }; }; //#endregion //#region src/json.ts /** * Returns a JSON feed * @param ins */ var json_default = (ins) => { const { options, items, extensions } = ins; const feed = { version: "https://jsonfeed.org/version/1", title: options.title }; if (options.link) feed.home_page_url = options.link; if (options.feedLinks && options.feedLinks.json) feed.feed_url = options.feedLinks.json; if (options.description) feed.description = options.description; if (options.image) feed.icon = options.image; if (options.author) { feed.author = {}; if (options.author.name) feed.author.name = options.author.name; if (options.author.link) feed.author.url = options.author.link; if (options.author.avatar) feed.author.avatar = options.author.avatar; } extensions.map((e) => { feed[e.name] = e.objects; }); feed.items = items.map((item) => { const feedItem = { id: item.id, content_html: item.content ?? item.description }; if (item.link) feedItem.url = item.link; if (item.title) feedItem.title = item.title; if (item.description && item.content) feedItem.summary = item.description; if (item.image) feedItem.image = item.image; if (item.date) feedItem.date_modified = item.date.toISOString(); if (item.published) feedItem.date_published = item.published.toISOString(); if (item.author) { let author = item.author; if (author instanceof Array) author = author[0]; feedItem.author = {}; if (author.name) feedItem.author.name = author.name; if (author.link) feedItem.author.url = author.link; if (author.avatar) feedItem.author.avatar = author.avatar; } if (Array.isArray(item.category)) { feedItem.tags = []; item.category.map((category) => { if (category.name) feedItem.tags.push(category.name); }); } if (item.extensions) item.extensions.map((e) => { feedItem[e.name] = e.objects; }); return feedItem; }); return JSON.stringify(feed, null, 4); }; //#endregion //#region src/rss2.ts /** * Returns a RSS 2.0 feed */ var rss2_default = (ins) => { const { options, extensions } = ins; let isAtom = false; let isContent = false; const base = { _declaration: { _attributes: { version: "1.0", encoding: "utf-8" } }, rss: { _attributes: { version: "2.0" }, channel: { title: { _text: options.title }, link: { _text: sanitize(options.link) }, description: { _text: options.description }, lastBuildDate: { _text: options.updated ? options.updated.toUTCString() : new Date().toUTCString() }, docs: { _text: options.docs ? options.docs : "https://validator.w3.org/feed/docs/rss2.html" }, generator: { _text: options.generator || generator } } } }; /** * Channel language * https://validator.w3.org/feed/docs/rss2.html#ltlanguagegtSubelementOfLtchannelgt */ if (options.language) base.rss.channel.language = { _text: options.language }; /** * Channel ttl * https://validator.w3.org/feed/docs/rss2.html#ltttlgtSubelementOfLtchannelgt */ if (options.ttl) base.rss.channel.ttl = { _text: options.ttl }; /** * Channel Image * https://validator.w3.org/feed/docs/rss2.html#ltimagegtSubelementOfLtchannelgt */ if (options.image) base.rss.channel.image = { title: { _text: options.title }, url: { _text: options.image }, link: { _text: sanitize(options.link) } }; /** * Channel Copyright * https://validator.w3.org/feed/docs/rss2.html#optionalChannelElements */ if (options.copyright) base.rss.channel.copyright = { _text: options.copyright }; /** * Channel Categories * https://validator.w3.org/feed/docs/rss2.html#comments */ ins.categories.map((category) => { if (!base.rss.channel.category) base.rss.channel.category = []; base.rss.channel.category.push({ _text: category }); }); /** * Feed URL * http://validator.w3.org/feed/docs/warning/MissingAtomSelfLink.html */ const atomLink = options.feed || options.feedLinks && options.feedLinks.rss; if (atomLink) { isAtom = true; base.rss.channel["atom:link"] = [{ _attributes: { href: sanitize(atomLink), rel: "self", type: "application/rss+xml" } }]; } /** * Hub for PubSubHubbub * https://code.google.com/p/pubsubhubbub/ */ if (options.hub) { isAtom = true; if (!base.rss.channel["atom:link"]) base.rss.channel["atom:link"] = []; base.rss.channel["atom:link"] = { _attributes: { href: sanitize(options.hub), rel: "hub" } }; } /** * Channel Categories * https://validator.w3.org/feed/docs/rss2.html#hrelementsOfLtitemgt */ base.rss.channel.item = []; ins.items.map((entry) => { const item = {}; if (entry.title) item.title = { _cdata: entry.title }; if (entry.link) item.link = { _text: sanitize(entry.link) }; if (entry.guid) item.guid = { _text: entry.guid }; else if (entry.id) item.guid = { _text: entry.id }; else if (entry.link) item.guid = { _text: sanitize(entry.link) }; if (entry.date) item.pubDate = { _text: entry.date.toUTCString() }; if (entry.published) item.pubDate = { _text: entry.published.toUTCString() }; if (entry.description) item.description = { _cdata: entry.description }; if (entry.content) { isContent = true; item["content:encoded"] = { _cdata: entry.content }; } /** * Item Author * https://validator.w3.org/feed/docs/rss2.html#ltauthorgtSubelementOfLtitemgt */ if (Array.isArray(entry.author)) { item.author = []; entry.author.map((author) => { if (author.email && author.name) item.author.push({ _text: author.email + " (" + author.name + ")" }); }); } /** * Item Category * https://validator.w3.org/feed/docs/rss2.html#ltcategorygtSubelementOfLtitemgt */ if (Array.isArray(entry.category)) { item.category = []; entry.category.map((category) => { item.category.push(formatCategory(category)); }); } /** * Item Enclosure * https://validator.w3.org/feed/docs/rss2.html#ltenclosuregtSubelementOfLtitemgt */ if (entry.enclosure) item.enclosure = formatEnclosure(entry.enclosure); if (entry.image) item.enclosure = formatEnclosure(entry.image, "image"); if (entry.audio) { let duration = void 0; if (options.podcast && typeof entry.audio !== "string" && entry.audio.duration) { duration = entry.audio.duration; entry.audio.duration = void 0; } item.enclosure = formatEnclosure(entry.audio, "audio"); if (duration) item["itunes:duration"] = formatDuration(duration); } if (entry.video) item.enclosure = formatEnclosure(entry.video, "video"); if (entry.extensions) entry.extensions.forEach((extension) => { item[extension.name] = extension.objects; }); base.rss.channel.item.push(item); }); if (isContent) { base.rss._attributes["xmlns:dc"] = "http://purl.org/dc/elements/1.1/"; base.rss._attributes["xmlns:content"] = "http://purl.org/rss/1.0/modules/content/"; } if (extensions) extensions.map((e) => { base.rss.channel[e.name] = e.objects; }); if (isAtom) base.rss._attributes["xmlns:atom"] = "http://www.w3.org/2005/Atom"; /** * Podcast extensions * https://support.google.com/podcast-publishers/answer/9889544?hl=en */ if (options.podcast) { base.rss._attributes["xmlns:googleplay"] = "http://www.google.com/schemas/play-podcasts/1.0"; base.rss._attributes["xmlns:itunes"] = "http://www.itunes.com/dtds/podcast-1.0.dtd"; if (options.category) { base.rss.channel["googleplay:category"] = options.category; base.rss.channel["itunes:category"] = options.category; } if (options.author?.email) { base.rss.channel["googleplay:owner"] = options.author.email; base.rss.channel["itunes:owner"] = { "itunes:email": options.author.email }; } if (options.author?.name) { base.rss.channel["googleplay:author"] = options.author.name; base.rss.channel["itunes:author"] = options.author.name; } if (options.image) base.rss.channel["googleplay:image"] = { _attributes: { href: sanitize(options.image) } }; } return convert.js2xml(base, { compact: true, ignoreComment: true, spaces: 4 }); }; /** * Returns a formated enclosure * @param enclosure * @param mimeCategory */ const formatEnclosure = (enclosure, mimeCategory = "image") => { if (typeof enclosure === "string") { const type$1 = new URL(sanitize(enclosure)).pathname.split(".").slice(-1)[0]; return { _attributes: { url: enclosure, length: 0, type: `${mimeCategory}/${type$1}` } }; } const type = new URL(sanitize(enclosure.url)).pathname.split(".").slice(-1)[0]; return { _attributes: { length: 0, type: `${mimeCategory}/${type}`, ...enclosure } }; }; /** * Returns a formated category * @param category */ const formatCategory = (category) => { const { name, domain } = category; return { _text: name, _attributes: { domain } }; }; /** * Returns a formated duration from seconds * @param duration */ const formatDuration = (duration) => { const seconds = duration % 60; const totalMinutes = Math.floor(duration / 60); const minutes = totalMinutes % 60; const hours = Math.floor(totalMinutes / 60); const notHours = ("0" + minutes).substr(-2) + ":" + ("0" + seconds).substr(-2); return hours > 0 ? hours + ":" + notHours : notHours; }; //#endregion //#region src/feed.ts /** * Class used to generate Feeds */ var Feed = class { items = []; categories = []; contributors = []; extensions = []; constructor(options) { this.options = options; } /** * Add a feed item * @param item */ addItem = (item) => this.items.push(item); /** * Add a category * @param category */ addCategory = (category) => this.categories.push(category); /** * Add a contributor * @param contributor */ addContributor = (contributor) => this.contributors.push(contributor); /** * Adds an extension * @param extension */ addExtension = (extension) => this.extensions.push(extension); /** * Returns a Atom 1.0 feed */ atom1 = () => atom1_default(this); /** * Returns a RSS 2.0 feed */ rss2 = () => rss2_default(this); /** * Returns a JSON1 feed */ json1 = () => json_default(this); }; //#endregion export { Feed }; //# sourceMappingURL=feed.js.map