Skip to content

Commit 92d3075

Browse files
authored
feat(html/unstable): html() (#7130)
1 parent 3185156 commit 92d3075

4 files changed

Lines changed: 136 additions & 13 deletions

File tree

html/deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"./unstable-escape-css": "./unstable_escape_css.ts",
88
"./unstable-escape-js": "./unstable_escape_js.ts",
99
"./unstable-is-valid-custom-element-name": "./unstable_is_valid_custom_element_name.ts",
10-
"./named-entity-list.json": "./named_entity_list.json"
10+
"./named-entity-list.json": "./named_entity_list.json",
11+
"./unstable-html": "./unstable_html.ts"
1112
}
1213
}

html/unstable_html.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
/**
5+
* A template literal tag function for creating HTML strings with interpolated
6+
* values.
7+
*
8+
* @experimental **UNSTABLE**: New API, yet to be vetted.
9+
*
10+
* This function processes template literals and concatenates them with
11+
* interpolated values. Values are inserted as-is without any HTML escaping or
12+
* sanitization. Undefined values are treated as empty strings.
13+
*
14+
* > [!WARNING]
15+
* > **Security Warning**: This function does NOT escape HTML. When
16+
* > interpolating user-provided data, you must manually escape it to prevent
17+
* > XSS (Cross-Site Scripting) attacks. Only use this function with trusted
18+
* > data or data that has been properly sanitized. Use
19+
* > {@linkcode https://jsr.io/@std/html/doc/~/escape | escape()} for escaping.
20+
*
21+
* @param strings The template string array containing the static parts of the template
22+
* @param values The values to be interpolated into the template
23+
* @returns The resulting HTML string with interpolated values
24+
*
25+
* @example Usage with trusted content
26+
* ```ts
27+
* import { html } from "@std/html/unstable-html";
28+
* import { assertEquals } from "@std/assert/equals";
29+
*
30+
* const name = "Alice";
31+
* const color = "blue";
32+
* const htmlContent = html`
33+
* <div>
34+
* <h1>Hello, ${name}!</h1>
35+
* <p style="color: ${color};">Welcome to our site.</p>
36+
* </div>
37+
* `;
38+
*
39+
* assertEquals(htmlContent, `
40+
* <div>
41+
* <h1>Hello, Alice!</h1>
42+
* <p style="color: blue;">Welcome to our site.</p>
43+
* </div>
44+
* `);
45+
* ```
46+
*
47+
* @example Usage with untrusted content that needs to be escaped
48+
* ```ts
49+
* import { html } from "@std/html/unstable-html";
50+
* import { assertEquals } from "@std/assert/equals";
51+
* import { escape } from "@std/html/entities";
52+
*
53+
* // WARNING: This is vulnerable to XSS attacks!
54+
* const userInput = '<script>alert("XSS")</script>';
55+
* const unsafeHtml = html`<div>${userInput}</div>`;
56+
*
57+
* const safeHtml = html`<div>${escape(userInput)}</div>`;
58+
*
59+
* assertEquals(unsafeHtml, '<div><script>alert("XSS")</script></div>');
60+
* assertEquals(safeHtml, "<div>&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;</div>");
61+
* ```
62+
*/
63+
export function html(
64+
strings: TemplateStringsArray,
65+
...values: unknown[]
66+
): string {
67+
return strings.reduce(
68+
(result, str, i) => result + str + (values[i] ?? ""),
69+
"",
70+
);
71+
}

html/unstable_html_test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
import { assertEquals } from "@std/assert/equals";
3+
import { html } from "./unstable_html.ts";
4+
5+
Deno.test("html()", () => {
6+
const a = "red";
7+
const b = "blue";
8+
const result = html`
9+
<span style="color: ${a};">${b}</span>
10+
`;
11+
assertEquals(
12+
result,
13+
`
14+
<span style="color: red;">blue</span>
15+
`,
16+
);
17+
});
18+
19+
Deno.test("html() treats undefined as empty string", () => {
20+
assertEquals(
21+
html`
22+
<div>${undefined}</div>
23+
`,
24+
`
25+
<div></div>
26+
`,
27+
);
28+
});
29+
30+
Deno.test("html() with no interpolations", () => {
31+
assertEquals(
32+
html`
33+
<p>hello</p>
34+
`,
35+
`
36+
<p>hello</p>
37+
`,
38+
);
39+
});
40+
41+
Deno.test("html() with empty string interpolation", () => {
42+
assertEquals(
43+
html`
44+
<div>${""}</div>
45+
`,
46+
`
47+
<div></div>
48+
`,
49+
);
50+
});
51+
52+
Deno.test("html() does not escape HTML in interpolated values", () => {
53+
const userInput = '<script>alert("XSS")</script>';
54+
assertEquals(
55+
html`
56+
<div>${userInput}</div>
57+
`,
58+
`
59+
<div><script>alert("XSS")</script></div>
60+
`,
61+
);
62+
});

http/file_server.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { getNetworkAddress } from "@std/net/unstable-get-network-address";
5555
import { escape } from "@std/html/entities";
5656
import { HEADER } from "./unstable_header.ts";
5757
import { METHOD } from "./unstable_method.ts";
58+
import { html } from "@std/html/unstable-html";
5859

5960
interface EntryInfo {
6061
mode: string;
@@ -438,18 +439,6 @@ function createBaseHeaders(): Headers {
438439
});
439440
}
440441

441-
function html(
442-
strings: TemplateStringsArray,
443-
...values: unknown[]
444-
): string {
445-
let out = "";
446-
for (let i = 0; i < strings.length; ++i) {
447-
out += strings[i];
448-
if (i < values.length) out += values[i] ?? "";
449-
}
450-
return out;
451-
}
452-
453442
function dirViewerTemplate(dirname: string, entries: EntryInfo[]): string {
454443
const splitDirname = dirname.split("/").filter((path) => Boolean(path));
455444
const headerPaths = ["home", ...splitDirname];

0 commit comments

Comments
 (0)