Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion html/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
71 changes: 71 additions & 0 deletions html/unstable_html.ts
Original file line number Diff line number Diff line change
@@ -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`
* <div>
* <h1>Hello, ${name}!</h1>
* <p style="color: ${color};">Welcome to our site.</p>
* </div>
* `;
*
* assertEquals(htmlContent, `
* <div>
* <h1>Hello, Alice!</h1>
* <p style="color: blue;">Welcome to our site.</p>
* </div>
* `);
* ```
*
* @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 = '<script>alert("XSS")</script>';
* const unsafeHtml = html`<div>${userInput}</div>`;
*
* const safeHtml = html`<div>${escape(userInput)}</div>`;
*
* assertEquals(unsafeHtml, '<div><script>alert("XSS")</script></div>');
* assertEquals(safeHtml, "<div>&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;</div>");
* ```
*/
export function html(
strings: TemplateStringsArray,
...values: unknown[]
): string {
return strings.reduce(
(result, str, i) => result + str + (values[i] ?? ""),
"",
);
}
62 changes: 62 additions & 0 deletions html/unstable_html_test.ts
Original file line number Diff line number Diff line change
@@ -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`
<span style="color: ${a};">${b}</span>
`;
assertEquals(
result,
`
<span style="color: red;">blue</span>
`,
);
});

Deno.test("html() treats undefined as empty string", () => {
assertEquals(
html`
<div>${undefined}</div>
`,
`
<div></div>
`,
);
});

Deno.test("html() with no interpolations", () => {
assertEquals(
html`
<p>hello</p>
`,
`
<p>hello</p>
`,
);
});

Deno.test("html() with empty string interpolation", () => {
assertEquals(
html`
<div>${""}</div>
`,
`
<div></div>
`,
);
});

Deno.test("html() does not escape HTML in interpolated values", () => {
const userInput = '<script>alert("XSS")</script>';
assertEquals(
html`
<div>${userInput}</div>
`,
`
<div><script>alert("XSS")</script></div>
`,
);
});
13 changes: 1 addition & 12 deletions http/file_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand Down
Loading