diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 10ff0afb..002e8b8c 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -41,7 +41,7 @@ export { useComponent, useState } from "./component_node"; export { status } from "./status"; export { reactive, markRaw, toRaw } from "./reactivity"; export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks"; -export { batched, EventBus, whenReady, loadFile, markup } from "./utils"; +export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils"; export { onWillStart, onMounted, diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts index 7445fa47..ccdbc13f 100644 --- a/src/runtime/utils.ts +++ b/src/runtime/utils.ts @@ -111,15 +111,15 @@ export async function loadFile(url: string): Promise { */ export class Markup extends String {} -function _escapeHtml(str: any): string | Markup { +export function htmlEscape(str: any): Markup { if (str instanceof Markup) { return str; } if (str === undefined) { - return ""; + return markup(""); } if (typeof str === "number") { - return String(str); + return markup(String(str)); } [ ["&", "&"], @@ -131,7 +131,7 @@ function _escapeHtml(str: any): string | Markup { ].forEach((pairs) => { str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]); }); - return str; + return markup(str); } /* @@ -153,7 +153,7 @@ export function markup( let acc = ""; let i = 0; for (; i < placeholders.length; ++i) { - acc += strings[i] + _escapeHtml(placeholders[i]); + acc += strings[i] + htmlEscape(placeholders[i]); } acc += strings[i]; return new Markup(acc); diff --git a/tests/utils.test.ts b/tests/utils.test.ts index bc9097b2..44466d6b 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -1,4 +1,4 @@ -import { batched, EventBus, markup } from "../src/runtime/utils"; +import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils"; import { nextMicroTick } from "./helpers"; describe("event bus behaviour", () => { @@ -78,6 +78,61 @@ describe("markup", () => { const html = markup("Hello"); expect(html).toBeInstanceOf(Markup); }); + describe("htmlEscape", () => { + test("htmlEscape escapes text", () => { + const res = htmlEscape("

test

"); + expect(res.toString()).toBe("<p>test</p>"); + expect(res).toBeInstanceOf(Markup); + }); + test("htmlEscape keeps html markup", () => { + const res = htmlEscape(markup("

test

")); + expect(res.toString()).toBe("

test

"); + expect(res).toBeInstanceOf(Markup); + }); + test("htmlEscape produces empty string on undefined", () => { + const res = htmlEscape(undefined); + expect(res.toString()).toBe(""); + expect(res).toBeInstanceOf(Markup); + }); + test("htmlEscape produces string from number", () => { + const res = htmlEscape(10); + expect(res.toString()).toBe("10"); + expect(res).toBeInstanceOf(Markup); + }); + test("htmlEscape produces string from boolean", () => { + const res = htmlEscape(false); + expect(res.toString()).toBe("false"); + expect(res).toBeInstanceOf(Markup); + }); + test("htmlEscape correctly escapes various links", () => { + expect(htmlEscape("this is a link").toString()).toBe( + "<a>this is a link</a>" + ); + expect(htmlEscape(`odoo`).toString()).toBe( + `<a href="https://www.odoo.com">odoo<a>` + ); + expect(htmlEscape(`odoo`).toString()).toBe( + `<a href='https://www.odoo.com'>odoo<a>` + ); + expect(htmlEscape("Odoo`s website").toString()).toBe( + `<a href='https://www.odoo.com'>Odoo`s website<a>` + ); + }); + test("htmlEscape doesn't escape already escaped content", () => { + const res = htmlEscape("

test

"); + expect(res.toString()).toBe("<p>test</p>"); + expect(res).toBeInstanceOf(Markup); + const res2 = htmlEscape(res); + expect(res2.toString()).toBe("<p>test</p>"); + expect(res2).toBeInstanceOf(Markup); + expect(res2).toBe(res); + }); + test("htmlEscape returns markup even for only-safe text", () => { + const res = htmlEscape("safe"); + expect(res.toString()).toBe("safe"); + expect(res).toBeInstanceOf(Markup); + }); + }); describe("tag function", () => { test("interpolated values are escaped", () => { const maliciousInput = ""; @@ -99,5 +154,11 @@ describe("markup", () => { const html = markup``; expect(html.toString()).toBe(``); }); + test("already escaped content is not escaped again", () => { + const res = htmlEscape("

test

"); + expect(res.toString()).toBe("<p>test</p>"); + const html = markup`${res}`; + expect(html.toString()).toBe("<p>test</p>"); + }); }); });