aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib/vendor/rss/rss.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/vendor/rss/rss.ts')
-rw-r--r--src/lib/vendor/rss/rss.ts231
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'