diff options
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/vendor/rss/rss.example.md | 110 | ||||
| -rw-r--r-- | src/lib/vendor/rss/rss.ts | 231 | ||||
| -rw-r--r-- | src/lib/vendor/rss/xml.example.md | 242 | ||||
| -rw-r--r-- | src/lib/vendor/rss/xml.ts | 144 |
4 files changed, 0 insertions, 727 deletions
diff --git a/src/lib/vendor/rss/rss.example.md b/src/lib/vendor/rss/rss.example.md deleted file mode 100644 index 4e532fd..0000000 --- a/src/lib/vendor/rss/rss.example.md +++ /dev/null @@ -1,110 +0,0 @@ -```ts -const doc = new XMLDocumentRoot().child( - new XMLDeclaration().version().encoding(), - new RSSRootElement() - .channel( - new RSSChannelElement( - 'Latest blog posts for 7222e800', - 'Some Description Here', - 'https://estrogen.zone/~mem/blog/' - ) - .pubDate(new Date('2026-01-14T15:53:57Z') /* When the last item was published */) - .lastBuildDate(new Date() /* When this file was last updated - usually when your build process last ran */) - .language('en') - .child(new XMLElement('nonStandardElement').attribute('non-standard', 'true')) - .items( - new RSSItemElement( - 'Some Fancy Blog Post Title', - 'Imagine some crazy fun thing here that is guaranteed to get the reader hooked. A really good blurb would be here in practice.', - 'https://estrogen.zone/~mem/blog/1234567890-your-fancy-post-slug/', - ) - .author('7222e800') - .guid('https://estrogen.zone/~mem/blog/1234567890', true) - .pubDate(new Date('2026-01-14T15:53:57Z')), - new RSSItemElement( - 'Domesticated Catgirl Transport', - 'Smuggling multiple thousands of catgirls over borders isn\'t an easy feat. Here\'s how we did it.', - 'https://estrogen.zone/~mem/blog/1234566789-catgirl-smuggling-operation/', - ) - .author('CatgirlSmuggler9000') - .author('7222e800') - .guid('https://estrogen.zone/~mem/blog/1234566789', true) - .pubDate(new Date('2026-01-14T15:53:57Z')), - ) - ) -); -const xml = doc.toString(); -``` - -will output - -```xml -<?xml version="1.0" encoding="UTF-8" ?> -<rss version="2.0" - xmlns:content="http://purl.org/rss/1.0/modules/content/"> - <channel> - <title> - Latest blog posts for 7222e800 - </title> - <description> - Some Description Here - </description> - <link> - https://estrogen.zone/~mem/blog/ - </link> - <pubDate> - Wed, 14 Jan 2026 15:53:57 GMT - </pubDate> - <lastBuildDate> - Mon, 26 Jan 2026 04:45:27 GMT - </lastBuildDate> - <language> - en - </language> - <nonStandardElement non-standard="true" /> - <item> - <title> - Some Fancy Blog Post Title - </title> - <description> - Imagine some crazy fun thing here that is guaranteed to get the reader hooked. A really good blurb would be here in practice. - </description> - <link> - https://estrogen.zone/~mem/blog/1234567890-your-fancy-post-slug/ - </link> - <author> - 7222e800 - </author> - <guid isPermaLink="true"> - https://estrogen.zone/~mem/blog/1234567890 - </guid> - <pubDate> - Wed, 14 Jan 2026 15:53:57 GMT - </pubDate> - </item> - <item> - <title> - Domesticated Catgirl Transport - </title> - <description> - Smuggling multiple thousands of catgirls over borders isn't an easy feat. Here's how we did it. - </description> - <link> - https://estrogen.zone/~mem/blog/1234566789-catgirl-smuggling-operation/ - </link> - <author> - CatgirlSmuggler9000 - </author> - <author> - 7222e800 - </author> - <guid isPermaLink="true"> - https://estrogen.zone/~mem/blog/1234566789 - </guid> - <pubDate> - Wed, 14 Jan 2026 15:32:57 GMT - </pubDate> - </item> - </channel> -</rss> -``` diff --git a/src/lib/vendor/rss/rss.ts b/src/lib/vendor/rss/rss.ts deleted file mode 100644 index f44a6d2..0000000 --- a/src/lib/vendor/rss/rss.ts +++ /dev/null @@ -1,231 +0,0 @@ -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' diff --git a/src/lib/vendor/rss/xml.example.md b/src/lib/vendor/rss/xml.example.md deleted file mode 100644 index 591904a..0000000 --- a/src/lib/vendor/rss/xml.example.md +++ /dev/null @@ -1,242 +0,0 @@ -# xml example - -If you like directly working with XML, here's an example of how to for this: - -```ts -const posts = [ - { - title: 'Launching SSH during early boot with mkinitfs', - url: 'https://estrogen.zone/~mem/blog/1768406136-alpine-ssh-early-initfs-disk-decryption/', - blurb: 'Replacing the early init with our own script to launch SSH, killing it in early userspace, and allowing remote disk decryption in the mean time', - author: '7222e800', - guid: 'https://estrogen.zone/~mem/blog/1768406136', - published: new Date('2026-01-14T15:53:57Z').toUTCString() - } -]; -const doc = new XMLDocumentRoot().child( - new XMLDeclaration().version().encoding(), - new XMLRootElement("rss") - .attribute("version", "2.0") - .xmlns("content", "http://purl.org/rss/1.0/modules/content/") - .child( - new XMLElement('channel') - .child( - new XMLElement('title').child(new XMLText('Latest blog posts for 7222e800')), - new XMLElement('link').child(new XMLText('https://estrogen.zone/~mem/blog/')), - new XMLElement('description').child(new XMLText('Some Description')), - new XMLElement('pubDate').child(new XMLText(new Date().toUTCString())), - ...posts.map(post => new XMLElement('item').child( - new XMLElement('title').child(new XMLText(post.title)), - new XMLElement('link').child(new XMLText(post.url)), - new XMLElement('description').child(new XMLText(post.blurb)), - new XMLElement('author').child(new XMLText(post.author)), - new XMLElement('guid').child(new XMLText(post.guid)), - new XMLElement('published').child(new XMLText(post.published)), - )) - ) - ) -); -console.log( - util.inspect( - doc, - { compact: false, colors: true, breakLength: 80, depth: 90 }, true - ) -); -console.log(doc.toString()); -``` - -as of writing, will output this internal state: - -```log -XMLDocumentRoot { - attributes: Map(0) {}, - children: [ - XMLDeclaration { - attributes: Map(2) { - 'version' => '1.0', - 'encoding' => 'UTF-8' - }, - children: [], - tagType: '#declaration' - }, - XMLRootElement { - attributes: Map(2) { - 'version' => '2.0', - 'xmlns:content' => 'http://purl.org/rss/1.0/modules/content/' - }, - children: [ - XMLElement { - attributes: Map(0) {}, - children: [ - XMLElement { - attributes: Map(0) {}, - children: [ - XMLText { - tagType: '#text', - text: 'Latest blog posts for 7222e800' - } - ], - tagType: '#element', - tagName: 'title' - }, - XMLElement { - attributes: Map(0) {}, - children: [ - XMLText { - tagType: '#text', - text: 'https://estrogen.zone/~mem/blog/' - } - ], - tagType: '#element', - tagName: 'link' - }, - XMLElement { - attributes: Map(0) {}, - children: [ - XMLText { - tagType: '#text', - text: 'Some Description' - } - ], - tagType: '#element', - tagName: 'description' - }, - XMLElement { - attributes: Map(0) {}, - children: [ - XMLText { - tagType: '#text', - text: 'Mon, 26 Jan 2026 03:34:31 GMT' - } - ], - tagType: '#element', - tagName: 'pubDate' - }, - XMLElement { - attributes: Map(0) {}, - children: [ - XMLElement { - attributes: Map(0) {}, - children: [ - XMLText { - tagType: '#text', - text: 'Launching SSH during early boot with mkinitfs' - } - ], - tagType: '#element', - tagName: 'title' - }, - XMLElement { - attributes: Map(0) {}, - children: [ - XMLText { - tagType: '#text', - text: 'https://estrogen.zone/~mem/blog/1768406136-alpine-ssh-early-initfs-disk-decryption/' - } - ], - tagType: '#element', - tagName: 'link' - }, - XMLElement { - attributes: Map(0) {}, - children: [ - XMLText { - tagType: '#text', - text: 'Replacing the early init with our own script to launch SSH, killing it in early userspace, and allowing remote disk decryption in the mean time' - } - ], - tagType: '#element', - tagName: 'description' - }, - XMLElement { - attributes: Map(0) {}, - children: [ - XMLText { - tagType: '#text', - text: '7222e800' - } - ], - tagType: '#element', - tagName: 'author' - }, - XMLElement { - attributes: Map(0) {}, - children: [ - XMLText { - tagType: '#text', - text: 'https://estrogen.zone/~mem/blog/1768406136' - } - ], - tagType: '#element', - tagName: 'guid' - }, - XMLElement { - attributes: Map(0) {}, - children: [ - XMLText { - tagType: '#text', - text: 'Wed, 14 Jan 2026 15:53:57 GMT' - } - ], - tagType: '#element', - tagName: 'published' - } - ], - tagType: '#element', - tagName: 'item' - } - ], - tagType: '#element', - tagName: 'channel' - } - ], - tagType: '#element', - tagName: 'rss' - } - ], - tagType: '#document' -} -``` - -and this RSS: - -```xml -<?xml version="1.0" encoding="UTF-8" ?> -<rss version="2.0" - xmlns:content="http://purl.org/rss/1.0/modules/content/"> - <channel> - <title> - Latest blog posts for 7222e800 - </title> - <link> - https://estrogen.zone/~mem/blog/ - </link> - <description> - Some Description - </description> - <pubDate> - Mon, 26 Jan 2026 03:34:31 GMT - </pubDate> - <item> - <title> - Launching SSH during early boot with mkinitfs - </title> - <link> - https://estrogen.zone/~mem/blog/1768406136-alpine-ssh-early-initfs-disk-decryption/ - </link> - <description> - Replacing the early init with our own script to launch SSH, killing it in early userspace, and allowing remote disk decryption in the mean time - </description> - <author> - 7222e800 - </author> - <guid> - https://estrogen.zone/~mem/blog/1768406136 - </guid> - <published> - Wed, 14 Jan 2026 15:53:57 GMT - </published> - </item> - </channel> -</rss> -``` diff --git a/src/lib/vendor/rss/xml.ts b/src/lib/vendor/rss/xml.ts deleted file mode 100644 index 8a0d2c4..0000000 --- a/src/lib/vendor/rss/xml.ts +++ /dev/null @@ -1,144 +0,0 @@ -// String methods based on https://github.com/cburgmer/xmlserializer/blob/master/xmlserializer.js - -export const removeInvalidXMLCharacters = (content: string) => - // See http://www.w3.org/TR/xml/#NT-Char for valid XML 1.0 characters - content.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); - -export const serializeAttributeValue = (value: string) => value - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - -export const serializeTextContent = (content: string) => content - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>'); - -export const serializeAttribute = (name: string, value: string) => name + '="' + serializeAttributeValue(value) + '"'; -export const indent = (text: string, size: number) => text.split('\n').map(line => `${' '.repeat(size)}${line}`).join('\n') - -export enum XMLTagType { - DocumentRoot = '#document', - XMLDeclaration = '#declaration', - Element = '#element', - Text = '#text', - Comment = '#comment', - CData = '#cdata-section', -} -export abstract class BaseXMLTag { - public abstract readonly tagType: XMLTagType; - public abstract toString(): string; -} -export abstract class XMLTagWithAttributes extends BaseXMLTag { - public attributes = new Map<string, string>(); - protected get attributeString() { - let str = ''; - for (const segment of this.attributes.entries().map(([k, v]) => serializeAttribute(k, v))) { - if (str && str.length + segment.length > 60) - str += `\n${indent(segment, 2)}`; - else - str += ` ${segment}`; - } - return str; - } - public attribute(name: string, value: string) { - this.attributes.set(name, value); - return this; - } - public getAttribute(name: string) { - return this.attributes.get(name); - } -} -export abstract class XMLTagWithChildrenAndAttributes extends XMLTagWithAttributes { - public children: XMLTag[] = []; - protected get contentString() { - return this.children.map(v => v.toString()).join('\n'); - } - public child(...children: XMLTag[]) { - this.children.push(...children); - return this; - } -} -export class XMLDocumentRoot extends XMLTagWithChildrenAndAttributes { - public readonly tagType = XMLTagType.DocumentRoot as const; - public toString(): string { - return removeInvalidXMLCharacters(this.contentString); - } -} -export class XMLDeclaration extends XMLTagWithChildrenAndAttributes { - public readonly tagType = XMLTagType.XMLDeclaration as const; - public toString(): string { - return `<?xml${this.attributeString} ?>`; - } - public version(version = "1.0") { - return this.attribute("version", version) - } - public encoding(encoding = "UTF-8") { - return this.attribute("encoding", encoding); - } -}; -export class XMLElement extends XMLTagWithChildrenAndAttributes { - public readonly tagType = XMLTagType.Element as const; - public constructor(public tagName: string) { super() } - protected findElementChild(query: (element: XMLElement) => boolean) { - return this.children.find(el => el.tagType === XMLTagType.Element && query(el)) as XMLElement - } - public toString(): string { - return `<${this.tagName}${this.attributeString}${this.children.length ? `> -${indent(this.contentString, 2)} -</${this.tagName}>` : ' />'}` - } -} -export class XMLRootElement extends XMLElement { - /** - * @param subns Leave as empty string for root namespace - */ - public xmlns(subns: string, value: string) { - this.attribute(`xmlns${subns ? `:${subns}` : ''}`, value) - return this; - } -} -export class XMLText extends BaseXMLTag { - public tagType = XMLTagType.Text as const; - public constructor(public text: string) { super() } - public toString(): string { - return serializeTextContent(this.text); - } -} -export class XMLComment extends BaseXMLTag { - public tagType = XMLTagType.Comment as const; - public constructor( - public text: string, - /** Set to 0 for no wrapping at all */ - public wrapAt = 70, - public addSpacesAroundCommentTags = true - ) { super() } - public toString(): string { - let text = this.text; - if (this.wrapAt) { - const lines = text.split('\n') - const newLines = [] as string[] - if (lines.length === 1 && lines[0].length < this.wrapAt) { - if (this.addSpacesAroundCommentTags) - newLines[0] = ` ${text} ` - } else - for (let line of lines) - while (line.length) { - newLines.push(line.substring(0, this.wrapAt)) - line = line.substring(this.wrapAt) - } - } else if (this.addSpacesAroundCommentTags) this.text = ` ${this.text} ` - return `<!--${text - .replace(/-/g, '-')}-->`; - } -} -export class XMLCData extends BaseXMLTag { - public tagType = XMLTagType.CData as const; - public constructor(public content: string) { super() } - public toString(): string { - return `<![CDATA[${this.content.replace(/\]\]>/gu, ']]]]><![CDATA[>')}]]>`; - } -} -export type XMLTag = XMLDocumentRoot | XMLDeclaration | XMLElement | XMLText | XMLComment | XMLCData; |