Files
abode/src/util/xmlwriter.ts
T
codingetandCodex 31d4636dde
CI / install-and-build (pull_request) Successful in 1m29s
CI / format (pull_request) Successful in 51s
CI / typecheck-source (pull_request) Successful in 41s
CI / typecheck-tests (pull_request) Successful in 39s
CI / test (pull_request) Successful in 51s
CI / lint (pull_request) Successful in 21s
ci: add pull request quality gates
Co-Authored-By: gpt-5.6-terra <noreply@openai.com>
2026-07-22 21:50:29 +00:00

208 lines
6.0 KiB
TypeScript

const escapes = {
'"': "&quot;",
"'": "&apos;",
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
};
function parseAdd<T>(
rest: (Record<string, string> | string | ((writer: XmlWriter) => T))[],
): [
props?: Record<string, string>,
content?: string,
children?: (writer: XmlWriter) => T,
] {
let props: Record<string, string> | 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 = '<?xml version="1.0" encoding="utf-8"?>';
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<string, string>,
content?: string,
children?: NonNullable<unknown>,
) {
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) +
"</" +
tag +
">" +
(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 += "</" + tag + ">" + (this.#indent ? "\n" : "");
} else {
this.#content += "/>" + (this.#indent ? "\n" : "");
}
}
add(tag: string): XmlWriter;
add(tag: string, content: string): XmlWriter;
add(tag: string, props: Record<string, string>): XmlWriter;
add(tag: string, props: Record<string, string>, content: string): XmlWriter;
add(tag: string, children: (writer: XmlWriter) => void): XmlWriter;
add(
tag: string,
props: Record<string, string>,
children: (writer: XmlWriter) => void,
): XmlWriter;
add(
tag: string,
...rest: (Record<string, string> | 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<void>;
addAsync(tag: string, content: string): Promise<void>;
addAsync(tag: string, props: Record<string, string>): Promise<void>;
addAsync(
tag: string,
props: Record<string, string>,
content: string,
): Promise<void>;
addAsync(
tag: string,
children: (writer: XmlWriter) => Promise<void>,
): Promise<void>;
addAsync(
tag: string,
props: Record<string, string>,
children: (writer: XmlWriter) => Promise<void>,
): Promise<void>;
async addAsync(
tag: string,
...rest: (
Record<string, string> | string | ((writer: XmlWriter) => Promise<void>)
)[]
): Promise<void> {
const [props, content, children] = parseAdd(rest);
this.#addPre(tag, props, content, children);
if (children) {
await children(this);
this.#addPost(tag);
}
}
static build(
options: NonNullable<ConstructorParameters<typeof XmlWriter>[0]>,
...rest: Parameters<InstanceType<typeof XmlWriter>["add"]>
): string;
static build(
...rest: Parameters<InstanceType<typeof XmlWriter>["add"]>
): string;
static build(
...rest:
| Parameters<InstanceType<typeof XmlWriter>["add"]>
| [
NonNullable<ConstructorParameters<typeof XmlWriter>[0]>,
...Parameters<InstanceType<typeof XmlWriter>["add"]>,
]
): string {
let options: ConstructorParameters<typeof XmlWriter>[0];
if (typeof rest[0] === "object") {
options = rest.shift()! as ConstructorParameters<typeof XmlWriter>[0];
}
return new XmlWriter(options).add(
...(rest as Parameters<InstanceType<typeof XmlWriter>["add"]>),
).content;
}
static buildAsync(
options: NonNullable<ConstructorParameters<typeof XmlWriter>[0]>,
...rest: Parameters<InstanceType<typeof XmlWriter>["addAsync"]>
): Promise<string>;
static buildAsync(
...rest: Parameters<InstanceType<typeof XmlWriter>["addAsync"]>
): Promise<string>;
static async buildAsync(
...rest:
| Parameters<InstanceType<typeof XmlWriter>["addAsync"]>
| [
NonNullable<ConstructorParameters<typeof XmlWriter>[0]>,
...Parameters<InstanceType<typeof XmlWriter>["addAsync"]>,
]
): Promise<string> {
let options: ConstructorParameters<typeof XmlWriter>[0];
if (typeof rest[0] === "object") {
options = rest.shift()! as ConstructorParameters<typeof XmlWriter>[0];
}
const writer = new XmlWriter(options);
await writer.addAsync(
...(rest as Parameters<InstanceType<typeof XmlWriter>["addAsync"]>),
);
return writer.content;
}
static escape = escapeXml;
}
export const buildXml = XmlWriter.build;
export const buildXmlAsync = XmlWriter.buildAsync;