/** Markup that is already safe: built by `html`, or rendered by markdown-it with raw HTML off. */
export class Html {
constructor(readonly value: string) {}
toString(): string {
return this.value;
}
}
export type Part = Html | string | number | null | undefined | false | Part[];
const ENTITIES: Record = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
/** The one escaping helper (ยง18): every interpolated value goes through it unless it is `Html`. */
export function escapeHtml(text: string): string {
return text.replace(/[&<>"']/g, (c) => ENTITIES[c] as string);
}
function render(part: Part): string {
if (part instanceof Html) return part.value;
if (Array.isArray(part)) return part.map(render).join("");
if (part === null || part === undefined || part === false) return "";
return escapeHtml(String(part));
}
export function html(strings: TemplateStringsArray, ...values: Part[]): Html {
let out = strings[0] as string;
values.forEach((value, i) => {
out += render(value) + strings[i + 1];
});
return new Html(out);
}
/** Trusts markup produced elsewhere; only for markdown-it output. */
export function trusted(markup: string): Html {
return new Html(markup);
}