diff --git a/html/deno.json b/html/deno.json
index 07fb93d2a146..cb1849947e5a 100644
--- a/html/deno.json
+++ b/html/deno.json
@@ -7,6 +7,7 @@
"./unstable-escape-css": "./unstable_escape_css.ts",
"./unstable-escape-js": "./unstable_escape_js.ts",
"./unstable-is-valid-custom-element-name": "./unstable_is_valid_custom_element_name.ts",
- "./named-entity-list.json": "./named_entity_list.json"
+ "./named-entity-list.json": "./named_entity_list.json",
+ "./unstable-html": "./unstable_html.ts"
}
}
diff --git a/html/unstable_html.ts b/html/unstable_html.ts
new file mode 100644
index 000000000000..c01fd2616979
--- /dev/null
+++ b/html/unstable_html.ts
@@ -0,0 +1,71 @@
+// Copyright 2018-2026 the Deno authors. MIT license.
+// This module is browser compatible.
+
+/**
+ * A template literal tag function for creating HTML strings with interpolated
+ * values.
+ *
+ * @experimental **UNSTABLE**: New API, yet to be vetted.
+ *
+ * This function processes template literals and concatenates them with
+ * interpolated values. Values are inserted as-is without any HTML escaping or
+ * sanitization. Undefined values are treated as empty strings.
+ *
+ * > [!WARNING]
+ * > **Security Warning**: This function does NOT escape HTML. When
+ * > interpolating user-provided data, you must manually escape it to prevent
+ * > XSS (Cross-Site Scripting) attacks. Only use this function with trusted
+ * > data or data that has been properly sanitized. Use
+ * > {@linkcode https://jsr.io/@std/html/doc/~/escape | escape()} for escaping.
+ *
+ * @param strings The template string array containing the static parts of the template
+ * @param values The values to be interpolated into the template
+ * @returns The resulting HTML string with interpolated values
+ *
+ * @example Usage with trusted content
+ * ```ts
+ * import { html } from "@std/html/unstable-html";
+ * import { assertEquals } from "@std/assert/equals";
+ *
+ * const name = "Alice";
+ * const color = "blue";
+ * const htmlContent = html`
+ *
+ *
Hello, ${name}!
+ *
Welcome to our site.
+ *
+ * `;
+ *
+ * assertEquals(htmlContent, `
+ *
+ *
Hello, Alice!
+ *
Welcome to our site.
+ *
+ * `);
+ * ```
+ *
+ * @example Usage with untrusted content that needs to be escaped
+ * ```ts
+ * import { html } from "@std/html/unstable-html";
+ * import { assertEquals } from "@std/assert/equals";
+ * import { escape } from "@std/html/entities";
+ *
+ * // WARNING: This is vulnerable to XSS attacks!
+ * const userInput = '';
+ * const unsafeHtml = html`${userInput}
`;
+ *
+ * const safeHtml = html`${escape(userInput)}
`;
+ *
+ * assertEquals(unsafeHtml, '');
+ * assertEquals(safeHtml, "<script>alert("XSS")</script>
");
+ * ```
+ */
+export function html(
+ strings: TemplateStringsArray,
+ ...values: unknown[]
+): string {
+ return strings.reduce(
+ (result, str, i) => result + str + (values[i] ?? ""),
+ "",
+ );
+}
diff --git a/html/unstable_html_test.ts b/html/unstable_html_test.ts
new file mode 100644
index 000000000000..cf99b0d523c7
--- /dev/null
+++ b/html/unstable_html_test.ts
@@ -0,0 +1,62 @@
+// Copyright 2018-2026 the Deno authors. MIT license.
+import { assertEquals } from "@std/assert/equals";
+import { html } from "./unstable_html.ts";
+
+Deno.test("html()", () => {
+ const a = "red";
+ const b = "blue";
+ const result = html`
+ ${b}
+ `;
+ assertEquals(
+ result,
+ `
+ blue
+ `,
+ );
+});
+
+Deno.test("html() treats undefined as empty string", () => {
+ assertEquals(
+ html`
+ ${undefined}
+ `,
+ `
+
+ `,
+ );
+});
+
+Deno.test("html() with no interpolations", () => {
+ assertEquals(
+ html`
+ hello
+ `,
+ `
+ hello
+ `,
+ );
+});
+
+Deno.test("html() with empty string interpolation", () => {
+ assertEquals(
+ html`
+ ${""}
+ `,
+ `
+
+ `,
+ );
+});
+
+Deno.test("html() does not escape HTML in interpolated values", () => {
+ const userInput = '';
+ assertEquals(
+ html`
+ ${userInput}
+ `,
+ `
+
+ `,
+ );
+});
diff --git a/http/file_server.ts b/http/file_server.ts
index 81a3de58f678..687dfa1340e0 100644
--- a/http/file_server.ts
+++ b/http/file_server.ts
@@ -55,6 +55,7 @@ import { getNetworkAddress } from "@std/net/unstable-get-network-address";
import { escape } from "@std/html/entities";
import { HEADER } from "./unstable_header.ts";
import { METHOD } from "./unstable_method.ts";
+import { html } from "@std/html/unstable-html";
interface EntryInfo {
mode: string;
@@ -438,18 +439,6 @@ function createBaseHeaders(): Headers {
});
}
-function html(
- strings: TemplateStringsArray,
- ...values: unknown[]
-): string {
- let out = "";
- for (let i = 0; i < strings.length; ++i) {
- out += strings[i];
- if (i < values.length) out += values[i] ?? "";
- }
- return out;
-}
-
function dirViewerTemplate(dirname: string, entries: EntryInfo[]): string {
const splitDirname = dirname.split("/").filter((path) => Boolean(path));
const headerPaths = ["home", ...splitDirname];