diff --git a/CHANGELOG.md b/CHANGELOG.md
index f37f425..32ca651 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [v1.0.2] - 2026-01-05
+
+### Added
+- Add `createPolicy()` to create a `TrustedTypePolicy` with a `createHTML()` that escapes untrusted input
+
## [v1.0.1] - 2026-01-05
### Added
diff --git a/html.js b/html.js
index c73ecc1..9068dac 100644
--- a/html.js
+++ b/html.js
@@ -22,6 +22,13 @@ const _str = (str, fallback = '') => (str?.toString?.() ?? fallback);
export const escapeHTML = str => _str(str)
.replaceAll(HTML_UNSAFE_PATTERN, char => HTML_REPLACEMENTS[char]);
+/**
+ * Tagged template function that automatically escapes interpolated values.
+ * @example html`
${content}
`
+ */
+export function html(strings, ...values) {
+ return String.raw(strings, ...values.map(escapeHTML));
+}
/**
* Escapes characters that are unsafe or prohibited in HTML attribute names.
diff --git a/html.test.js b/html.test.js
index bfda068..6b95ef1 100644
--- a/html.test.js
+++ b/html.test.js
@@ -1,6 +1,6 @@
import assert from 'node:assert';
import { describe, test, before } from 'node:test';
-import { escapeHTML, escapeAttrName, stringifyAttr } from './html.js';
+import { escapeHTML, escapeAttrName, stringifyAttr, html } from './html.js';
// Mock DOM Attr class for Node environment
class MockAttr {
@@ -114,3 +114,23 @@ describe('Security Escaping Utils', () => {
});
});
});
+
+describe('html tagged template', () => {
+ test('escapes interpolated values', () => {
+ const malicious = '');
+ assert.strictEqual(result, '<script>alert(1)</script>');
+ });
+
+ test('Array Usage: joins and escapes list items', () => {
+ // Essential to ensure arrays are not stringified with commas
+ const items = ['
', 'bold'];
+ const result = html`Items: ${items}`;
+
+ assert.strictEqual(result, 'Items: <br>,<b>bold</b>');
+ });
+
+ test('Security: enforces Double Escaping in fallback mode', () => {
+ /* * CRITICAL SECURITY CHECK:
+ * Since we are in Node (no TrustedTypes), the output of `html` is just a string.
+ * If we nest this string into another template, it MUST be escaped again.
+ * If it wasn't, we would be vulnerable to "fake trust".
+ */
+ const inner = html`Safe`;
+ // inner is string: "Safe"
+
+ const outer = html`${inner}
`;
+ // outer sees a string, so it escapes it: "<span>Safe</span>
"
+
+ assert.strictEqual(outer, '<span>Safe</span>
');
+ });
+
+ test('Interface: returns a policy-like object', () => {
+ // Ensures the return value matches the TrustedTypePolicy interface shape
+ assert.strictEqual(typeof policy.createHTML, 'function');
+ assert.strictEqual(policy.name, 'default');
+ });
+});