From eef25d360d7c22abcdf2d0e53a7614aa3bbe393d Mon Sep 17 00:00:00 2001 From: "M. George Hansen" Date: Sat, 2 Jan 2021 21:24:17 -0800 Subject: [PATCH] feat: add dangerouslySetInnerHTML attr This adds are new attribute to Elements, "dangerouslySetInnerHTML", which like the same attribute from React allows one to take a stirng of unescaped HTML and render it unconditionally. This is of course a potentially dangerous operation that can open your app up to XSS attacks, but for interoperating with existing content management systems and libraries that output HTML (e.g. markdown renderers). Using "dangerouslySetInnerHTML" on an element with children will generate an error within createElement, since it doesn't make sense to have both children and inner HTML. --- src/create-element.ts | 9 +++++++++ src/jsx.ts | 9 ++++++++- src/render.ts | 13 +++++++++++-- test/test-suites/rendering.tsx | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/create-element.ts b/src/create-element.ts index d46308d..d2d4398 100644 --- a/src/create-element.ts +++ b/src/create-element.ts @@ -54,6 +54,15 @@ export function createElement( } const attrs: Record = {}; for (const [key, value] of Object.entries(props || {})) { + if (key === "dangerouslySetInnerHTML") { + if (children.length > 0) { + throw new Error( + 'An element with children may not have a "dangerouslySetInnerHTML" prop since children would be overriden', + ); + } + attrs[key] = value.__html; + continue; + } if ( typeof value !== "string" && typeof value !== "number" && diff --git a/src/jsx.ts b/src/jsx.ts index 55d69a9..d450c4b 100644 --- a/src/jsx.ts +++ b/src/jsx.ts @@ -25,7 +25,14 @@ export interface MicrodataAttributes { itemRef?: string; } -export interface HTMLAttributes extends RdfaAttributes, MicrodataAttributes { +export interface SetInnerHtmlAttributes { + dangerouslySetInnerHTML?: { __html: string }; +} + +export interface HTMLAttributes + extends RdfaAttributes, + MicrodataAttributes, + SetInnerHtmlAttributes { accept?: string; acceptCharset?: string; accessKey?: string; diff --git a/src/render.ts b/src/render.ts index 4566d70..60cdb73 100644 --- a/src/render.ts +++ b/src/render.ts @@ -34,8 +34,12 @@ const renderElement = (elem: Element): string => { let output = ""; output += startTag(elem); - for (const child of elem.children) { - output += renderElement(child); + if (elem.attributes.dangerouslySetInnerHTML != null) { + output += elem.attributes.dangerouslySetInnerHTML; + } else { + for (const child of elem.children) { + output += renderElement(child); + } } output += endTag(elem); return output; @@ -55,6 +59,11 @@ const startTag = (elem: HTMLElement): string => { continue; } + // Ignore the special attr for setting raw inner HTML. + if (attrName === "dangerouslySetInnerHTML") { + continue; + } + let normalizedAttrName = escapeHtml(attrName.toLowerCase()); if (normalizedAttrName === "classname") { normalizedAttrName = "class"; diff --git a/test/test-suites/rendering.tsx b/test/test-suites/rendering.tsx index 7caa8b2..ed3dfe5 100644 --- a/test/test-suites/rendering.tsx +++ b/test/test-suites/rendering.tsx @@ -117,4 +117,37 @@ testSuite("renderPage", ({ test, expect }) => { "
test of
fragments
", ); }); + + test("renders unescaped HTML via dangerouslySetInnerHTML", () => { + const html = renderPage( + +
red alert!
", + }} + /> + , + ); + expect(html).toEqual( + "
red alert!
", + ); + }); + + test("throws error when both dangerouslySetInnerHTML and children prop present", () => { + expect(() => + renderPage( + +
set phasers to kill
", + }} + > +
set phasers to stun
+ + , + ), + ).toThrowErrorMatching( + 'An element with children may not have a "dangerouslySetInnerHTML" prop since children would be overriden', + ); + }); });