diff options
Diffstat (limited to 'src/utility/feed')
| -rw-r--r-- | src/utility/feed/atom.ts | 87 | ||||
| -rw-r--r-- | src/utility/feed/helper.ts | 67 | ||||
| -rw-r--r-- | src/utility/feed/index.ts | 12 | ||||
| -rw-r--r-- | src/utility/feed/json.ts | 41 | ||||
| -rw-r--r-- | src/utility/feed/rss.ts | 85 |
5 files changed, 292 insertions, 0 deletions
diff --git a/src/utility/feed/atom.ts b/src/utility/feed/atom.ts new file mode 100644 index 0000000..d8827e9 --- /dev/null +++ b/src/utility/feed/atom.ts @@ -0,0 +1,87 @@ + + + +/*** UTILITY ------------------------------------------ ***/ + +import { BaseFeed, escapeXML, type FeedOptions } from "./helper.ts"; + +interface AtomEntry { + content?: { + body: string; + type?: string; + }; + id: string; + image?: string; + link: string; + summary: string; + title: string; + updated?: Date; +} + +/*** EXPORT ------------------------------------------- ***/ + +export class FeedAtom extends BaseFeed<AtomEntry> { + constructor(options: FeedOptions) { + super(options); + } + + build(): string { + const xmlParts: string[] = [ + `<?xml version="1.0" encoding="UTF-8"?>\n`, + `<feed xmlns="http://www.w3.org/2005/Atom">\n`, + ` <title>${escapeXML(this.options.title)}</title>\n`, + ` <subtitle>${escapeXML(this.options.description)}</subtitle>\n`, + ` <link rel="alternate" href="${escapeXML(this.options.link)}"/>\n`, + ` <link rel="self" href="${escapeXML(this.options.link)}"/>\n`, + ` <updated>${this.options.updated?.toISOString()}</updated>\n`, + ` <generator>${this.options.generator || "the webb blog"}</generator>\n`, + ` <id>${escapeXML(this.options.link)}</id>\n` + ]; + + for (const author of this.options.authors) { + xmlParts.push(` <author>\n`); + + if (author.name) + xmlParts.push(` <name>${escapeXML(author.name)}</name>\n`); + + if (author.email) + xmlParts.push(` <email>${escapeXML(author.email)}</email>\n`); + + if (author.link) + xmlParts.push(` <uri>${escapeXML(author.link)}</uri>\n`); + + xmlParts.push(` </author>\n`); + } + + if (this.options.icon) { + xmlParts.push(` <icon>${escapeXML(this.options.icon)}</icon>\n`); + xmlParts.push(` <logo>${escapeXML(this.options.icon)}</logo>\n`); + } + + if (this.options.feed) + xmlParts.push(` <link rel="self" href="${escapeXML(this.options.feed)}"/>\n`); + + for (const entry of this.items) { + xmlParts.push( + ` <entry>\n`, + ` <title type="html">${escapeXML(entry.title)}</title>\n`, + ` <link href="${escapeXML(entry.link)}"/>\n`, + ` <id>${escapeXML(entry.id)}</id>\n`, + ` <updated>${entry.updated?.toISOString() || new Date().toISOString()}</updated>\n`, + ` <summary type="html">${escapeXML(entry.summary)}</summary>\n`, + ` <content type="${entry.content?.type || "html"}">${escapeXML(entry.content?.body || entry.summary)}</content>\n`, + entry.image ? + ` <media:thumbnail url="${escapeXML(entry.image)}" />\n` : + "", + ` </entry>\n` + ); + } + + for (const category of this.categories) { + xmlParts.push(` <category term="${escapeXML(category)}"/>\n`); + } + + xmlParts.push("</feed>"); + return xmlParts.join(""); + } +} diff --git a/src/utility/feed/helper.ts b/src/utility/feed/helper.ts new file mode 100644 index 0000000..0177364 --- /dev/null +++ b/src/utility/feed/helper.ts @@ -0,0 +1,67 @@ + + + +/*** EXPORT ------------------------------------------- ***/ + +export interface FeedOptions { + authors: Array<{ + name: string; + email?: string; + link?: string; + }>; + copyright?: string; + description: string; + feed?: string; + feedLinks?: { + atom?: string; + }; + generator?: string; + icon?: string; + id?: string; + language?: string; + link: string; + title: string; + updated?: Date; +} + +export function escapeXML(unsafe: string): string { + const escapeMap: { [key: string]: string } = { + "<": "<", + ">": ">", + "&": "&", + "'": "'", + '"': """ + }; + + return unsafe.replace(/[<>&'"]/g, (c) => escapeMap[c] || c); +} + +export abstract class BaseFeed<T> { + protected categories: Set<string>; + protected items: Array<T>; + protected options: FeedOptions; + + constructor(options: FeedOptions) { + this.categories = new Set(); + this.items = []; + + this.options = { + ...options, + updated: options.updated || new Date() + }; + } + + abstract build(): string; + + addCategory(category: string) { + this.categories.add(category); + } + + addContributor(contributor: { name: string; email: string; link?: string }) { + this.options.authors.push(contributor); + } + + addItem(item: T) { + this.items.push(item); + } +} diff --git a/src/utility/feed/index.ts b/src/utility/feed/index.ts new file mode 100644 index 0000000..d58609a --- /dev/null +++ b/src/utility/feed/index.ts @@ -0,0 +1,12 @@ + + + +/*** EXPORT ------------------------------------------- ***/ + +export { FeedAtom as ATOM } from "./atom.ts"; +export { FeedJSON as JSON } from "./json.ts"; +export { FeedRSS as RSS } from "./rss.ts"; + + + +/*** forked from https://github.com/GabsEdits/feed ***/ diff --git a/src/utility/feed/json.ts b/src/utility/feed/json.ts new file mode 100644 index 0000000..589aeae --- /dev/null +++ b/src/utility/feed/json.ts @@ -0,0 +1,41 @@ + + + +/*** UTILITY ------------------------------------------ ***/ + +import { BaseFeed, type FeedOptions } from "./helper.ts"; + +interface JsonItem { + content_html?: string; + date_published?: Date; + id: string; + title: string; + url: string; +} + +/*** EXPORT ------------------------------------------- ***/ + +export class FeedJSON extends BaseFeed<JsonItem> { + constructor(options: FeedOptions) { + super(options); + } + + build(): string { + const json: Record<string, unknown> = { + feed_url: this.options.feed, + home_page_url: this.options.link, + icon: this.options.icon, + items: this.items.map(({ content_html, date_published, id, title, url }) => ({ + ...(content_html && { content_html }), + date_published: date_published?.toISOString() || new Date().toUTCString, + id, + title, + url + })), + title: this.options.title, + version: "https://jsonfeed.org/version/1" + }; + + return JSON.stringify(json, null, 2); + } +} diff --git a/src/utility/feed/rss.ts b/src/utility/feed/rss.ts new file mode 100644 index 0000000..c3971bb --- /dev/null +++ b/src/utility/feed/rss.ts @@ -0,0 +1,85 @@ + + + +/*** UTILITY ------------------------------------------ ***/ + +import { BaseFeed, escapeXML, type FeedOptions } from "./helper.ts"; + +interface RssItem { + content: { + body: string; + }; + description: string; + id: string; + image?: string; + link: string; + title: string; + updated?: Date; +} + +/*** EXPORT ------------------------------------------- ***/ + +export class FeedRSS extends BaseFeed<RssItem> { + constructor(options: FeedOptions) { + super(options); + } + + build(): string { + const xmlParts: string[] = [ + `<?xml version="1.0" encoding="UTF-8"?>\n`, + `<rss 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/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:wfw="http://wellformedweb.org/CommentAPI/">\n`, + ` <channel>\n`, + ` <title><![CDATA[${escapeXML(this.options.title)}]]></title>\n`, + ` <description><![CDATA[${escapeXML(this.options.description)}]]></description>\n`, + ` <link>${escapeXML(this.options.link)}</link>\n`, + ` <lastBuildDate>${this.options.updated?.toUTCString()}</lastBuildDate>\n`, + ` <language>${this.options.language || "en-US"}</language>\n`, + ` <generator>${escapeXML(this.options.generator || "the webb blog")}</generator>\n`, + ` <atom:link href="${escapeXML(this.options.link)}" rel="self" type="application/rss+xml"/>\n` + ]; + + if (this.options.authors.length > 0) { + const authorXml = this.options.authors.map((author) => { + const escapedEmail = author.email ? escapeXML(author.email) : ""; + const escapedName = author.name ? escapeXML(author.name) : ""; + const emailPart = escapedEmail ? `${escapedEmail} ` : ""; + + return ( + ` <webMaster>${emailPart}(${escapedName})</webMaster>\n` + + ` <managingEditor>${emailPart}(${escapedName})</managingEditor>\n` + ); + }).join(""); + + xmlParts.push(authorXml); + } + + if (this.items.length > 0) { + const itemsXml = this.items.map((item) => { + const contentXml = item.content ? + ` <content:encoded><![CDATA[${escapeXML(item.content.body)}]]></content:encoded>\n` : + ""; + + const imageXml = item.image ? + ` <media:thumbnail url="${escapeXML(item.image)}" />\n` : + ""; + + return ( + ` <item>\n` + + ` <title>${escapeXML(item.title)}</title>\n` + + ` <link>${escapeXML(item.link)}</link>\n` + + ` <guid>${escapeXML(item.id)}</guid>\n` + + ` <pubDate>${item.updated?.toUTCString() || new Date().toUTCString()}</pubDate>\n` + + ` <description>${escapeXML(item.description)}</description>\n` + + contentXml + + imageXml + + ` </item>\n` + ); + }).join(""); + + xmlParts.push(itemsXml); + } + + xmlParts.push(` </channel>\n`, `</rss>`); + return xmlParts.join(""); + } +} |
