diff options
Diffstat (limited to 'src/lib/vendor/rss/rss.ts')
| -rw-r--r-- | src/lib/vendor/rss/rss.ts | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/src/lib/vendor/rss/rss.ts b/src/lib/vendor/rss/rss.ts new file mode 100644 index 0000000..f44a6d2 --- /dev/null +++ b/src/lib/vendor/rss/rss.ts @@ -0,0 +1,231 @@ +import { XMLElement, XMLRootElement, XMLTagType, XMLText } from './xml'; + +class XMLElementWithRSSUtil extends XMLElement { + protected replaceChildByTagName(tag: string) { + const existingEl = this.findElementChild(v => v.tagName === tag); + if (existingEl) { + existingEl.children.length = 0; + existingEl.attributes.clear(); + } + const el = existingEl ?? new XMLElement(tag); + if (!existingEl) + this.child(el) + return el + } +} +export class RSSItemElement extends XMLElementWithRSSUtil { + public constructor(title: string | null, description: string | null, link: string | URL | null) { + super("item"); + if (title !== null) + this.title(title); + if (description !== null) + this.description(description) + if (link !== null) + this.link(link) + } + /** Replaces the title of the item */ + public title(title: string) { + this.replaceChildByTagName('title').child(new XMLText(title)) + return this; + } + /** Replaces the link to the item */ + public link(link: string | URL) { + this.replaceChildByTagName('link').child(new XMLText(typeof link === 'string' ? link : link.href)) + return this; + } + /** Sets the description of the item, replacing it if it already exists */ + public description(description: string) { + this.replaceChildByTagName('description').child(new XMLText(description)) + return this; + } + /** Sets the URL for the comment section of an item, replacing it if it already exists */ + public comments(comments: string | URL) { + return this.child(new XMLElement('comments').child(new XMLText(typeof comments === 'string' ? comments : comments.href))); + } + /** Sets the timestamp where the most recent item in the channel was published, replacing it if it already exists */ + public pubDate(pubDate: Date) { + this.replaceChildByTagName('pubDate').child(new XMLText(pubDate.toUTCString())) + return this; + } + /** Specifies a third-party source for the item. */ + public source(sourceName: string, sourceUrl: string | URL) { + return this.child(new XMLElement('source').attribute('url', typeof sourceUrl === 'string' ? sourceUrl : sourceUrl.href).child(new XMLText(sourceName))) + } + /** Adds a category to the post */ + public category( + category: string, + ) { + return this.child(new XMLElement('category').child(new XMLText(category))); + } + /** Adds a author to the post */ + public author( + author: string, + ) { + return this.child(new XMLElement('author').child(new XMLText(author))); + } + /** Adds a unique ID to the post. This ID must be globally unique, but has no format specifications. */ + public guid( + id: string, + /** If this is a permanent link to the post. If true, this must be a URL that permanently points to this item. */ + isPermaLink: boolean + ) { + return this.child(new XMLElement('guid').child(new XMLText(id)).attribute('isPermaLink', isPermaLink ? 'true' : 'false')); + } + /** Adds a media file to be included with an item */ + public enclosure( + url: string, + mimeType: string, + /** in bytes */ + length: number + ) { + return this.child( + new XMLElement('enclosure') + .attribute('url', url) + .attribute('length', length.toString()) + .attribute('type', mimeType) + ) + } +} +export class RSSChannelElement extends XMLElementWithRSSUtil { + public constructor(title: string | null, description: string | null, link: URL | string | null) { + super("channel"); + if (title !== null) + this.title(title); + if (description !== null) + this.description(description) + if (link !== null) + this.link(link) + } + /** Replaces the title of the channel */ + public title(title: string) { + this.replaceChildByTagName('title').child(new XMLText(title)) + return this; + } + /** Replaces the link to the channel */ + public link(link: string | URL) { + this.replaceChildByTagName('link').child(new XMLText(typeof link === 'string' ? link : link.href)) + return this; + } + /** Sets the description of the channel, replacing it if it already exists */ + public description(description: string) { + this.replaceChildByTagName('description').child(new XMLText(description)) + return this; + } + /** Sets the timestamp where the most recent item in the channel was published, replacing it if it already exists */ + public pubDate(pubDate: Date) { + this.replaceChildByTagName('pubDate').child(new XMLText(pubDate.toUTCString())) + return this; + } + /** Sets the timestamp where the RSS file was last modified in any way, replacing it if it already exists */ + public lastBuildDate(lastBuildDate: Date) { + this.replaceChildByTagName('lastBuildDate').child(new XMLText(lastBuildDate.toUTCString())) + return this; + } + /** Sets the time to live for the channel - the maximum time until clients should re-fetch it, replacing it if it already exists */ + public ttl(ttl: number) { + this.replaceChildByTagName('ttl').child(new XMLText(ttl.toString())) + return this; + } + /** Adds the copyright information of the channel */ + public copyright(copyright: string) { + return this.child(new XMLElement('copyright').child(new XMLText(copyright))) + } + /** Adds the generator used for this RSS feed - we recommend calling this and keeping it at .rsstrogen() - Can be called multiple times */ + public generator(generator: string = 'rsstrogen') { + return this.child(new XMLElement('generator').child(new XMLText(generator))); + } + /** Adds the webmaster managing this feed's email address */ + public webmaster(webmaster: string) { + return this.child(new XMLElement('webMaster').child(new XMLText(webmaster))) + } + /** Adds the managing editor managing this feed's email address */ + public managingEditor(managingEditor: string) { + return this.child(new XMLElement('managingEditor').child(new XMLText(managingEditor))) + } + /** + * Adds a URL of an image to be displayed when aggregators present a feed + * + * **Note:** The image must be of type GIF, JPEG or PNG. + */ + public image(image: { + /** Defines the hyperlink to the website that offers the channel */ + link: string; + /** Defines the text to display if the image could not be shown and/or for screen readers */ + alt: string; + /** Specifies the URL to the image */ + url: URL | string; + /** Specifies the text in the HTML title attribute of the link around the image. Defaults to the value of alt */ + description?: string | null; + /** The resolution of the image. Per Spec: Default is 88x31, Max is 144x400. */ + resolution?: [width: number, height: number] + }) { + const img = new XMLElement('image').child( + new XMLElement('link').child(new XMLText(image.link)), + new XMLElement('title').child(new XMLText(image.alt)), + new XMLElement('url').child(new XMLText(typeof image.url === 'string' ? image.url : image.url.href)), + ); + if (image.description !== null) + img.child( + new XMLElement('description').child(new XMLText(image.description ?? image.alt)), + ); + if (image.resolution) + img.child( + new XMLElement('width').child(new XMLText(image.resolution[0].toString())), + new XMLElement('height').child(new XMLText(image.resolution[1].toString())), + ); + return this.child(img); + } + /** Sets (or specifies an additional) language for the feed */ + public language(language: RSSLanguageCode) { + return this.child(new XMLElement('language').child(new XMLText(language))); + } + /** Specifies a search engine/text input should be specified with the feed */ + public search(engine: { + /** @default "Search" */ + title?: string, + /** @example "Search Neobot Systems" */ + description: string, + /** @example "https://search.example.com/search?" */ + link: string, + /** @example "query" */ + param: string, + }) { + this.replaceChildByTagName('textInput').child( + new XMLElement('title').child(new XMLText(engine.title ?? 'Search')), + new XMLElement('description').child(new XMLText(engine.description)), + new XMLElement('link').child(new XMLText(engine.link)), + new XMLElement('name').child(new XMLText(engine.param)), + ); + return this; + } + /** Adds a category to the feed */ + public category( + category: string, + /** Optional - A string or URL that identifies a categorization taxonomy. */ + domain?: string + ) { + const cat = new XMLElement('category'); + if (domain !== undefined) cat.attribute('domain', domain) + this.child(cat.child(new XMLText(category))) + return this; + } + // TODO: add more tags + /** Adds items to the RSS feed */ + public items(...items: RSSItemElement[]) { + return this.child(...items); + } +} +export class RSSRootElement extends XMLRootElement { + public constructor() { + super("rss") + this + .attribute("version", "2.0") + .xmlns("content", "http://purl.org/rss/1.0/modules/content/") + } + public channel(...channels: RSSChannelElement[]) { + return this.child(...channels) + } +} + +/** @link https://www.rssboard.org/rss-language-codes */ +export type RSSLanguageCode = 'af' | 'sq' | 'eu' | 'be' | 'bg' | 'ca' | 'zh-cn' | 'zh-tw' | 'hr' | 'cs' | 'da' | 'nl' | 'nl-be' | 'nl-nl' | 'en' | 'en-au' | 'en-bz' | 'en-ca' | 'en-ie' | 'en-jm' | 'en-nz' | 'en-ph' | 'en-za' | 'en-tt' | 'en-gb' | 'en-us' | 'en-zw' | ' et' | 'fo' | 'fi' | 'fr' | 'fr-be' | 'fr-ca' | 'fr-fr' | 'fr-lu' | 'fr-mc' | 'fr-ch' | 'gl' | 'gd' | 'de' | 'de-at' | 'de-de' | 'de-li' | 'de-lu' | 'de-ch' | 'el' | 'haw' | 'hu' | 'is' | 'in' | 'ga' | 'it' | 'it-it' | 'it-ch' | 'ja' | 'ko' | 'mk' | 'no' | 'pl' | 'pt' | 'pt-br' | 'pt-pt' | 'ro' | 'ro-mo' | 'ro-ro' | 'ru' | 'ru-mo' | 'ru-ru' | 'sr' | 'sk' | 'sl' | 'es' | 'es-ar' | 'es-bo' | 'es-cl' | 'es-co' | 'es-cr' | 'es-do' | 'es-ec' | 'es-sv' | 'es-gt' | 'es-hn' | 'es-mx' | 'es-ni' | 'es-pa' | 'es-py' | 'es-pe' | 'es-pr' | 'es-es' | 'es-uy' | 'es-ve' | 'sv' | 'sv-fi' | 'sv-se' | 'tr' | 'uk' |