Skip to content

Instantly share code, notes, and snippets.

@gordonbrander
Created May 10, 2026 05:01
Show Gist options
  • Select an option

  • Save gordonbrander/631e868e469f83971e5eff3cda44968a to your computer and use it in GitHub Desktop.

Select an option

Save gordonbrander/631e868e469f83971e5eff3cda44968a to your computer and use it in GitHub Desktop.
html tagged template literals for server side
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { html, render, unescaped } from "./ssr-html.ts";
describe("render", () => {
it("returns the static string for a template with no holes", () => {
assert.equal(render(html`<p>hello</p>`), "<p>hello</p>");
});
it("returns an empty string for an empty template", () => {
assert.equal(render(html``), "");
});
it("does not escape characters in static template parts", () => {
assert.equal(render(html`<p>a & b</p>`), "<p>a & b</p>");
});
});
describe("escaping", () => {
it("escapes &, <, >, \", and ' in interpolated strings", () => {
const value = `&<>"'`;
assert.equal(
render(html`<p>${value}</p>`),
"<p>&amp;&lt;&gt;&quot;&#39;</p>",
);
});
it("escapes ampersands first to avoid double-escaping entities", () => {
assert.equal(render(html`<p>${"&lt;"}</p>`), "<p>&amp;lt;</p>");
});
it("coerces numbers to escaped strings", () => {
assert.equal(render(html`<p>${42}</p>`), "<p>42</p>");
});
it("coerces booleans to escaped strings", () => {
assert.equal(render(html`<p>${true}</p>`), "<p>true</p>");
assert.equal(render(html`<p>${false}</p>`), "<p>false</p>");
});
it("coerces bigints to escaped strings", () => {
assert.equal(render(html`<p>${10n}</p>`), "<p>10</p>");
});
it("escapes coerced object string forms", () => {
const obj = { toString: (): string => "<x>" };
assert.equal(render(html`<p>${obj}</p>`), "<p>&lt;x&gt;</p>");
});
});
describe("nullish holes", () => {
it("renders null as empty string", () => {
assert.equal(render(html`<p>${null}</p>`), "<p></p>");
});
it("renders undefined as empty string", () => {
assert.equal(render(html`<p>${undefined}</p>`), "<p></p>");
});
});
describe("nested templates", () => {
it("renders a nested template without escaping its markup", () => {
const inner = html`<em>${"<x>"}</em>`;
assert.equal(
render(html`<p>${inner}</p>`),
"<p><em>&lt;x&gt;</em></p>",
);
});
it("supports multiple levels of nesting", () => {
const a = html`<a>${"&"}</a>`;
const b = html`<b>${a}</b>`;
const c = html`<c>${b}</c>`;
assert.equal(render(c), "<c><b><a>&amp;</a></b></c>");
});
});
describe("array holes", () => {
it("renders an array of strings joined with no separator, escaping each", () => {
assert.equal(
render(html`<p>${["a", "<b>", "c"]}</p>`),
"<p>a&lt;b&gt;c</p>",
);
});
it("renders an array of templates concatenated", () => {
const items = ["a", "b", "c"].map((i) => html`<li>${i}</li>`);
assert.equal(
render(html`<ul>${items}</ul>`),
"<ul><li>a</li><li>b</li><li>c</li></ul>",
);
});
it("handles mixed arrays of strings, templates, and nullish values", () => {
const mixed = ["a", html`<b>${"<x>"}</b>`, null, undefined, "c"];
assert.equal(
render(html`<p>${mixed}</p>`),
"<p>a<b>&lt;x&gt;</b>c</p>",
);
});
it("renders an empty array as empty string", () => {
assert.equal(render(html`<p>${[]}</p>`), "<p></p>");
});
});
describe("unescaped", () => {
it("inserts the value verbatim", () => {
assert.equal(
render(html`<p>${unescaped("<b>ok</b>")}</p>`),
"<p><b>ok</b></p>",
);
});
it("does not escape ampersands or angle brackets", () => {
assert.equal(
render(html`<p>${unescaped("a & <b>")}</p>`),
"<p>a & <b></p>",
);
});
it("works inside arrays alongside escaped values", () => {
const parts = ["<x>", unescaped("<b>ok</b>")];
assert.equal(
render(html`<p>${parts}</p>`),
"<p>&lt;x&gt;<b>ok</b></p>",
);
});
});
describe("multiple holes", () => {
it("renders multiple holes without gaps between adjacent values", () => {
assert.equal(render(html`${"a"}${"b"}${"c"}`), "abc");
});
it("renders holes interleaved with static parts", () => {
assert.equal(
render(html`<a>${1}</a><b>${2}</b><c>${3}</c>`),
"<a>1</a><b>2</b><c>3</c>",
);
});
});
const TEMPLATE = Symbol("ssr-html.template");
const UNESCAPED = Symbol("ssr-html.unescaped");
/**
* An opaque, lazy representation of an HTML template produced by the {@link html}
* tag. Pass it to {@link render} to obtain a string, or interpolate it into
* another `html` template to nest it.
*/
export type Template = {
readonly [TEMPLATE]: true;
readonly strings: TemplateStringsArray;
readonly values: readonly unknown[];
};
/**
* A marker indicating that a string should be inserted into a template without
* HTML-escaping. Construct with {@link unescaped}. Only use for content you
* trust — interpolating untrusted input as `unescaped` is an XSS vulnerability.
*/
export type Unescaped = {
readonly [UNESCAPED]: true;
readonly value: string;
};
/**
* Tagged template literal that builds an HTML {@link Template}. Interpolated
* values are HTML-escaped at render time. Holes accept primitives, nested
* `html` templates, {@link unescaped} values, arrays of any of the above, and
* `null` / `undefined` (rendered as empty string).
*
* @example
* const name = "<world>";
* render(html`<h1>Hello, ${name}!</h1>`);
* // => "<h1>Hello, &lt;world&gt;!</h1>"
*/
export const html = (
strings: TemplateStringsArray,
...values: readonly unknown[]
): Template => ({ [TEMPLATE]: true, strings, values });
/**
* Wraps a string so that {@link render} inserts it verbatim, bypassing HTML
* escaping. Use for trusted, pre-rendered fragments (e.g. a doctype, an SVG
* blob). Never pass untrusted input — doing so allows XSS.
*
* @example
* render(html`<p>${unescaped("<b>bold</b>")}</p>`);
* // => "<p><b>bold</b></p>"
*/
export const unescaped = (value: string): Unescaped => ({
[UNESCAPED]: true,
value,
});
const escape = (s: string): string =>
s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
const isTemplate = (v: unknown): v is Template =>
typeof v === "object" && v !== null && (v as Record<symbol, unknown>)[TEMPLATE] === true;
const isUnescaped = (v: unknown): v is Unescaped =>
typeof v === "object" && v !== null && (v as Record<symbol, unknown>)[UNESCAPED] === true;
const renderValue = (value: unknown): string => {
if (value == null) return "";
if (isTemplate(value)) return render(value);
if (isUnescaped(value)) return value.value;
if (Array.isArray(value)) return value.map(renderValue).join("");
return escape(String(value));
};
/**
* Renders a {@link Template} to an HTML string. Interpolated values are
* escaped, nested templates are rendered recursively, arrays are concatenated
* with no separator, and `null` / `undefined` become empty strings. Values
* wrapped with {@link unescaped} are inserted verbatim.
*/
export const render = (template: Template): string => {
const { strings, values } = template;
let out = strings[0];
for (let i = 0; i < values.length; i++) {
out += renderValue(values[i]) + strings[i + 1];
}
return out;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment