const escapes = { '"': """, "'": "'", "<": "<", ">": ">", "&": "&", }; function parseAdd( rest: (Record | string | ((writer: XmlWriter) => T))[], ): [ props?: Record, content?: string, children?: (writer: XmlWriter) => T, ] { let props: Record | undefined; let children: ((writer: XmlWriter) => T) | undefined; let content: string | undefined; for (const item of rest) { if (typeof item === "function") children = item; else if (typeof item === "object") props = item; else content = item; } if (children && content) throw new Error("XML cannot have both children and text content"); return [props, content, children]; } export function escapeXml(text: string): string { return text.replaceAll(/["'<>&]/g, (x) => escapes[x as keyof typeof escapes]); } export class XmlWriter { #content: string; #indent: string | null; #stack: { tag: string; children: boolean }[]; constructor({ indent = false, header = true, }: { indent?: string | boolean; header?: string | boolean } = {}) { if (indent === true) indent = "\t"; this.#indent = indent || null; if (header === true) header = ''; this.#content = (header || "") + (this.#indent ? "\n" : ""); this.#stack = []; } get content(): string { if (this.#stack.length) throw new Error("In the middle of building"); return this.#content; } #addPre( tag: string, props?: Record, content?: string, children?: NonNullable, ) { const top = this.#stack.at(-1); if (top && !top.children) { top.children = true; this.#content += ">" + (this.#indent ? "\n" : ""); } if (this.#indent) this.#content += this.#indent.repeat(this.#stack.length); this.#content += "<" + tag; if (props) { for (const [key, value] of Object.entries(props)) { this.#content += " " + key + '="' + escapeXml(value) + '"'; } } if (content) { this.#content += ">" + escapeXml(content) + "" + (this.#indent ? "\n" : ""); } else if (children) { this.#stack.push({ tag, children: false }); } else { this.#content += "/>" + (this.#indent ? "\n" : ""); } } #addPost(tag: string) { const top = this.#stack.pop()!; if (top.children) { if (this.#indent) this.#content += this.#indent.repeat(this.#stack.length); this.#content += "" + (this.#indent ? "\n" : ""); } else { this.#content += "/>" + (this.#indent ? "\n" : ""); } } add(tag: string): XmlWriter; add(tag: string, content: string): XmlWriter; add(tag: string, props: Record): XmlWriter; add(tag: string, props: Record, content: string): XmlWriter; add(tag: string, children: (writer: XmlWriter) => void): XmlWriter; add( tag: string, props: Record, children: (writer: XmlWriter) => void, ): XmlWriter; add( tag: string, ...rest: (Record | string | ((writer: XmlWriter) => void))[] ): XmlWriter { const [props, content, children] = parseAdd(rest); this.#addPre(tag, props, content, children); if (children) { children(this); this.#addPost(tag); } return this; } addAsync(tag: string): Promise; addAsync(tag: string, content: string): Promise; addAsync(tag: string, props: Record): Promise; addAsync( tag: string, props: Record, content: string, ): Promise; addAsync( tag: string, children: (writer: XmlWriter) => Promise, ): Promise; addAsync( tag: string, props: Record, children: (writer: XmlWriter) => Promise, ): Promise; async addAsync( tag: string, ...rest: ( Record | string | ((writer: XmlWriter) => Promise) )[] ): Promise { const [props, content, children] = parseAdd(rest); this.#addPre(tag, props, content, children); if (children) { await children(this); this.#addPost(tag); } } static build( options: NonNullable[0]>, ...rest: Parameters["add"]> ): string; static build( ...rest: Parameters["add"]> ): string; static build( ...rest: | Parameters["add"]> | [ NonNullable[0]>, ...Parameters["add"]>, ] ): string { let options: ConstructorParameters[0]; if (typeof rest[0] === "object") { options = rest.shift()! as ConstructorParameters[0]; } return new XmlWriter(options).add( ...(rest as Parameters["add"]>), ).content; } static buildAsync( options: NonNullable[0]>, ...rest: Parameters["addAsync"]> ): Promise; static buildAsync( ...rest: Parameters["addAsync"]> ): Promise; static async buildAsync( ...rest: | Parameters["addAsync"]> | [ NonNullable[0]>, ...Parameters["addAsync"]>, ] ): Promise { let options: ConstructorParameters[0]; if (typeof rest[0] === "object") { options = rest.shift()! as ConstructorParameters[0]; } const writer = new XmlWriter(options); await writer.addAsync( ...(rest as Parameters["addAsync"]>), ); return writer.content; } static escape = escapeXml; } export const buildXml = XmlWriter.build; export const buildXmlAsync = XmlWriter.buildAsync;