From 7a3a7eb9c8e1012a75e6956ba476f13beb414f4c Mon Sep 17 00:00:00 2001 From: memdmp Date: Mon, 26 Jan 2026 04:36:52 +0100 Subject: feat: a bit of XML as a treat --- src/lib/vendor/rss/xml.example.md | 242 ++++++++++++++++++++++++++++++++++++++ src/lib/vendor/rss/xml.ts | 141 ++++++++++++++++++++++ 2 files changed, 383 insertions(+) create mode 100644 src/lib/vendor/rss/xml.example.md create mode 100644 src/lib/vendor/rss/xml.ts (limited to 'src') 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 + + + + + Latest blog posts for 7222e800 + + + https://estrogen.zone/~mem/blog/ + + + Some Description + + + Mon, 26 Jan 2026 03:34:31 GMT + + + + Launching SSH during early boot with mkinitfs + + + https://estrogen.zone/~mem/blog/1768406136 + + + 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 + + + 7222e800 + + + 1768406136 + + + Wed, 14 Jan 2026 15:53:57 GMT + + + + +``` 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, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +export const serializeTextContent = (content: string) => content + .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(); + 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 ``; + } + 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)} +` : ' />'}` + } +} +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 ``; + } +} +export class XMLCData extends BaseXMLTag { + public tagType = XMLTagType.CData as const; + public constructor(public content: string) { super() } + public toString(): string { + return `/gu, ']]]]>')}]]>`; + } +} +export type XMLTag = XMLDocumentRoot | XMLDeclaration | XMLElement | XMLComment; -- cgit v1.2.3