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'