feat: initial commit
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user