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
Co-Authored-By: gpt-5.6-terra <noreply@openai.com>
208 lines
6.0 KiB
TypeScript
208 lines
6.0 KiB
TypeScript
const escapes = {
|
|
'"': """,
|
|
"'": "'",
|
|
"<": "<",
|
|
">": ">",
|
|
"&": "&",
|
|
};
|
|
|
|
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;
|