From 2b9ba64d053ce10f196b156425db736b21cd02a5 Mon Sep 17 00:00:00 2001 From: Shinobu Hayashi Date: Thu, 18 Nov 2021 08:42:53 +0900 Subject: [PATCH] Feature(core): escape special text in SSR(and not escape with dangerousSetInnerHTML) (#44) * Feature: escape html with createTextNode * Chore: add test for escaping html * Feature: dangerouslySetInnerHTML without escaping html --- src/core.ts | 11 ++++++----- src/ssr.ts | 13 ++++++++++++- test/ssr.test.tsx | 29 +++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/core.ts b/src/core.ts index 1cb6b65..ce7ec1e 100644 --- a/src/core.ts +++ b/src/core.ts @@ -101,8 +101,6 @@ export const render = (component: any, parent: HTMLElement | null = null, remove }) else appendChildren(parent, _render(el)) } - //@ts-ignore - if (parent.ssr) return parent.ssr return parent } // returning one child or an array of children @@ -248,6 +246,11 @@ export const h = (tagNameOrComponent: any, props: any, ...children: any) => { // handle events else if (isEvent(element, p.toLowerCase())) element.addEventListener(p.toLowerCase().substring(2), (e: any) => props[p](e)) + else if (p === 'dangerouslySetInnerHTML') { + const fragment = document.createElement("fragment") + fragment.innerHTML = props[p].__html + element.appendChild(fragment) + } else if (/className/i.test(p)) console.warn('You can use "class" instead of "className".') else if (typeof props[p] !== 'undefined') element.setAttribute(p, props[p]) } @@ -255,7 +258,5 @@ export const h = (tagNameOrComponent: any, props: any, ...children: any) => { appendChildren(element, children) if (ref) ref(element) - // @ts-ignore - if (element.ssr) return element.ssr - return element + return element as any } diff --git a/src/ssr.ts b/src/ssr.ts index e78dfe8..539047b 100644 --- a/src/ssr.ts +++ b/src/ssr.ts @@ -37,6 +37,7 @@ export class HTMLElementSSR { ssr: string tagName: string isSelfClosing: boolean = false + nodeType: null | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 = null constructor(tag: string) { this.tagName = tag @@ -57,6 +58,8 @@ export class HTMLElementSSR { 'track', 'wbr' ] + + this.nodeType = 1 if (selfClosing.indexOf(tag) >= 0) { this.ssr = `<${tag} />` @@ -75,6 +78,10 @@ export class HTMLElementSSR { return reg.exec(this.ssr)?.[2] ?? '' } + set innerHTML(text) { + this.ssr = text + } + get innerText() { const reg = /(^<[^>]+>)(.+)?(<\/[a-z]+>$|\/>$)/gm return reg.exec(this.ssr)?.[2] ?? '' @@ -89,6 +96,10 @@ export class HTMLElementSSR { return { length: 1 } } + toString() { + return this.ssr + } + setAttributeNS(name: string, value: string) { this.setAttribute(name, value) } @@ -150,7 +161,7 @@ export class DocumentSSR { } createTextNode(text: string) { - return text + return escapeHtml(text) } querySelector(_query: string) { diff --git a/test/ssr.test.tsx b/test/ssr.test.tsx index 6703b60..bc54f58 100644 --- a/test/ssr.test.tsx +++ b/test/ssr.test.tsx @@ -89,7 +89,32 @@ test('should render without errors', async () => { }) test("should escape attribute's string value", () => { - const content = Nano.h('div', { id: '"hoge' }, 'span') + const content = Nano.h('div', { id: '"hoge' }, 'hoge') const html = renderSSR(content) - expect(html).toBe('
span
') + expect(html).toBe('
hoge
') +}) + +test('should escape text node', () => { + const content = Nano.h('div', {}, 'span') + const html = renderSSR(content) + expect(html).toBe('
<span>span</span>
') +}) + +test('should render dangerouslySetInnerHtml without escaping', () => { + const code = `
should not escape
` + const App = () => { + return ( +
+
hoge
+ +
+
+ ) + } + + const app = Nano.renderSSR() + const { body } = Helmet.SSR(app) + + expect(body).toBe(`
hoge
should not escape
`) + expect(spy).not.toHaveBeenCalled() })