const ENCODE_HTML_RULES: Record = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
const MATCH_HTML = /[&<>'"]/g;
function encodeCharacter(c: string) {
return ENCODE_HTML_RULES[c] || c;
}
/**
* Based on the `escapeXML` function from the `ejs` library.
*/
function escapeHtmlRaw(value: string): string {
return value == null ? '' : String(value).replaceAll(MATCH_HTML, encodeCharacter);
}
function escapeValue(value: unknown): string {
if (value instanceof HtmlSafeString) {
// Already escaped!
return value.toString();
} else if (Array.isArray(value)) {
return value.map((val) => escapeValue(val)).join('');
} else if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'bigint' ||
typeof value === 'boolean'
) {
return escapeHtmlRaw(String(value));
} else if (value == null) {
// undefined or null -- render nothing
return '';
} else if (typeof value === 'object') {
throw new Error(`Cannot interpolate object in template: ${JSON.stringify(value)}`);
} else {
// There shouldn't be any other types
throw new Error(
`Unexpected type in template: ${typeof value} for value ${JSON.stringify(value)}`,
);
}
}
// Based on https://github.com/Janpot/escape-html-template-tag
export class HtmlSafeString {
private readonly strings: readonly string[];
private readonly values: unknown[];
constructor(strings: readonly string[], values: unknown[]) {
this.strings = strings;
this.values = values;
}
toString(): string {
return this.values.reduce((acc, val, i) => {
return acc + escapeValue(val) + this.strings[i + 1];
}, this.strings[0]);
}
}
export type HtmlValue =
| string
| number
| boolean
| bigint
| HtmlSafeString
| undefined
| null
| HtmlValue[];
export function html(strings: TemplateStringsArray, ...values: HtmlValue[]): HtmlSafeString {
return new HtmlSafeString(strings, values);
}
/**
* Pre-escapes the rendered HTML. Useful for when you want to inline the HTML
* in something else, for instance in a `data-bs-content` attribute for a Bootstrap
* popover.
*/
export function escapeHtml(html: HtmlSafeString): HtmlSafeString {
return unsafeHtml(escapeHtmlRaw(html.toString()));
}
/**
* Will render the provided value without any additional escaping. Use carefully
* with user-provided data.
*
* @param value The value to render.
* @returns An {@link HtmlSafeString} representing the provided value.
*/
export function unsafeHtml(value: string): HtmlSafeString {
return new HtmlSafeString([value], []);
}
/**
* Joins a list of HTML values with a separator.
*
* @param values The values to join.
* @param separator The separator to use between values.
*/
export function joinHtml(values: HtmlValue[], separator: HtmlValue = ''): HtmlSafeString {
return unsafeHtml(values.map(escapeValue).join(escapeValue(separator)));
}