{
*/
export class Markup extends String {}
+function _escapeHtml(str: any): string | Markup {
+ if (str instanceof Markup) {
+ return str;
+ }
+ if (str === undefined) {
+ return "";
+ }
+ if (typeof str === "number") {
+ return String(str);
+ }
+ [
+ ["&", "&"],
+ ["<", "<"],
+ [">", ">"],
+ ["'", "'"],
+ ['"', """],
+ ["`", "`"],
+ ].forEach((pairs) => {
+ str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
+ });
+ return str;
+}
+
/*
* Marks a value as safe, that is, a value that can be injected as HTML directly.
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
+ *
+ * If called as a tag function, the interpolated strings are escaped.
*/
-export function markup(value: any) {
- return new Markup(value);
+export function markup(strings: TemplateStringsArray, ...placeholders: unknown[]): Markup;
+export function markup(value: string): Markup;
+export function markup(
+ valueOrStrings: string | TemplateStringsArray,
+ ...placeholders: unknown[]
+): Markup {
+ if (!Array.isArray(valueOrStrings)) {
+ return new Markup(valueOrStrings);
+ }
+ const strings = valueOrStrings;
+ let acc = "";
+ let i = 0;
+ for (; i < placeholders.length; ++i) {
+ acc += strings[i] + _escapeHtml(placeholders[i]);
+ }
+ acc += strings[i];
+ return new Markup(acc);
}
diff --git a/tests/utils.test.ts b/tests/utils.test.ts
index 29a109fa..bc9097b2 100644
--- a/tests/utils.test.ts
+++ b/tests/utils.test.ts
@@ -1,4 +1,4 @@
-import { batched, EventBus } from "../src/runtime/utils";
+import { batched, EventBus, markup } from "../src/runtime/utils";
import { nextMicroTick } from "./helpers";
describe("event bus behaviour", () => {
@@ -71,3 +71,33 @@ describe("batched", () => {
expect(n).toBe(2);
});
});
+
+const Markup = markup("").constructor;
+describe("markup", () => {
+ test("string is flagged as safe", () => {
+ const html = markup("");
+ expect(html).toBeInstanceOf(Markup);
+ });
+ describe("tag function", () => {
+ test("interpolated values are escaped", () => {
+ const maliciousInput = "";
+ const html = markup`${maliciousInput}`;
+ expect(html.toString()).toBe("<script>alert('💥💥')</script>");
+ expect(html).toBeInstanceOf(Markup);
+ });
+ test("interpolated markups aren't escaped", () => {
+ const shouldBeEscaped = "";
+ const shouldnt = markup("this is safe");
+ const html = markup`${shouldBeEscaped} ${shouldnt}
`;
+ expect(html.toString()).toBe(
+ "<script>alert('should be escaped')</script> this is safe
"
+ );
+ expect(html).toBeInstanceOf(Markup);
+ });
+ test("quotes in interpolated values are escaped", () => {
+ const imgUrl = `lol" onerror="alert('xss')`;
+ const html = markup`
`;
+ expect(html.toString()).toBe(`
`);
+ });
+ });
+});