aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/lib/vendor/rss/xml.example.md242
-rw-r--r--src/lib/vendor/rss/xml.ts141
2 files changed, 383 insertions, 0 deletions
diff --git a/src/lib/vendor/rss/xml.example.md b/src/lib/vendor/rss/xml.example.md
new file mode 100644
index 0000000..ba006e2
--- /dev/null
+++ b/src/lib/vendor/rss/xml.example.md
@@ -0,0 +1,242 @@
+# 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',
+ 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: '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'
+ }
+ ],
+ 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: '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
+ </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>
+ 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
new file mode 100644
index 0000000..30618c0
--- /dev/null
+++ b/src/lib/vendor/rss/xml.ts
@@ -0,0 +1,141 @@
+// 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, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&apos;');
+
+export const serializeTextContent = (content: string) => content
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+
+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: BaseXMLTag[] = [];
+ protected get contentString() {
+ return this.children.map(v => v.toString()).join('\n');
+ }
+ public child(...children: BaseXMLTag[]) {
+ 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() }
+ 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, '&#45;')}-->`;
+ }
+}
+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 | XMLComment;