From f803a53c0ef5986f4b82567e90ddf10c4f1facc6 Mon Sep 17 00:00:00 2001 From: James Hill Date: Thu, 28 Apr 2022 15:00:05 +0100 Subject: [PATCH 1/5] Refactored parseHtml into agnostic shared package --- packages/shared/src/parse.ts | 56 +++++++++++++++++++++++++++-- packages/shared/tests/parse.spec.ts | 22 +++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/parse.ts b/packages/shared/src/parse.ts index 4932cf5..7a4ff12 100644 --- a/packages/shared/src/parse.ts +++ b/packages/shared/src/parse.ts @@ -1,4 +1,4 @@ -import { IProps, CustomElement, ErrorTypes } from './model'; +import { IProps, CustomElement, ErrorTypes, selfClosingTags } from './model'; /* ----------------------------------- * @@ -25,6 +25,22 @@ function parseJson(this: CustomElement, value: string) { return result; } +/* ----------------------------------- + * + * parseHtml + * + * -------------------------------- */ + +function parseHtml(htmlString: string) { + const dom = getDocument(htmlString); + + if (!dom) { + return void 0; + } + + return domToArray(dom); +} + /* ----------------------------------- * * getDocument @@ -49,6 +65,42 @@ function getDocument(html: string) { return nodes.body; } +/* ----------------------------------- + * + * domToArray + * + * -------------------------------- */ + +function domToArray(node: Element) { + if(node.nodeType === 3) { + return [null, {}, node.textContent?.trim() || '']; + } + + if (node.nodeType !== 1) { + return []; + } + + const nodeName = String(node.nodeName).toLowerCase(); + const childNodes = Array.from(node.childNodes); + + const children = () => childNodes.map((child: Element) => domToArray(child)); + const props = getAttributeObject(node.attributes); + + if (nodeName === 'script') { + return []; + } + + if (nodeName === 'body') { + return [null, {}, children()]; + } + + if (selfClosingTags.includes(nodeName)) { + return [nodeName, props, []]; + } + + return [nodeName, props, children()]; +} + /* ----------------------------------- * * getAttributeObject @@ -114,4 +166,4 @@ function getPropKey(value: string) { * * -------------------------------- */ -export { parseJson, getDocument, getPropKey, getAttributeObject, getAttributeProps }; +export { parseJson, parseHtml, getDocument, getPropKey, getAttributeObject, getAttributeProps }; diff --git a/packages/shared/tests/parse.spec.ts b/packages/shared/tests/parse.spec.ts index 491e3ff..12c033e 100644 --- a/packages/shared/tests/parse.spec.ts +++ b/packages/shared/tests/parse.spec.ts @@ -1,6 +1,6 @@ import { h } from 'preact'; import { mount } from 'enzyme'; -import { parseJson, getPropKey } from '../src/parse'; +import { parseJson, parseHtml, getPropKey } from '../src/parse'; /* ----------------------------------- * @@ -11,6 +11,7 @@ import { parseJson, getPropKey } from '../src/parse'; const testHeading = 'testHeading'; const testData = { testHeading }; const testJson = JSON.stringify(testData); +const testHtml = `

${testHeading}


Hello there

`; /* ----------------------------------- * @@ -52,6 +53,25 @@ describe('parse', () => { }); }); + describe('parseHtml', () => { + it('correctly converts a DOM structure to data', () => { + const result = parseHtml(testHtml); + + console.log(result); + + expect(result).toEqual([null, {}, [ + ['h1', {}, [[null, {}, testHeading]]], + ['br', {}, []], + ['div', {}, [ + ['h2', { title: 'Main Title' }, [ + [null, {}, 'Hello'], + ['em', {}, [[null, {}, 'there']], + ]]], + ]] + ]]); + }); + }); + describe('getPropKey', () => { const testCamel = 'testSlot'; const testKebab = 'test-slot'; From 89513c7ef1b263b5cc172165e1b2601d80a05dfd Mon Sep 17 00:00:00 2001 From: James Hill Date: Thu, 28 Apr 2022 16:12:33 +0100 Subject: [PATCH 2/5] First working version of data to vdom conversion --- packages/preactement/src/define.ts | 4 +-- packages/preactement/src/parse.ts | 44 ++++++++---------------- packages/preactement/tests/parse.spec.ts | 12 ++++++- packages/shared/tests/parse.spec.ts | 4 +-- 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/packages/preactement/src/define.ts b/packages/preactement/src/define.ts index 953b9c2..c025b9a 100644 --- a/packages/preactement/src/define.ts +++ b/packages/preactement/src/define.ts @@ -10,7 +10,7 @@ import { getElementAttributes, getAsyncComponent, } from '@component-elements/shared'; -import { parseHtml } from './parse'; +import { parseChildren } from './parse'; import { IOptions, ComponentFunction } from './model'; /* ----------------------------------- @@ -131,7 +131,7 @@ function onConnected(this: CustomElement) { let children = this.__children; if (!this.__mounted && !this.hasAttribute('server')) { - children = h(parseHtml.call(this), {}); + children = h(parseChildren.call(this), {}); } this.__properties = { ...this.__slots, ...data, ...attributes }; diff --git a/packages/preactement/src/parse.ts b/packages/preactement/src/parse.ts index b946c0f..3c6f462 100644 --- a/packages/preactement/src/parse.ts +++ b/packages/preactement/src/parse.ts @@ -1,26 +1,20 @@ import { h, ComponentFactory, Fragment } from 'preact'; -import { - CustomElement, - getDocument, - getAttributeObject, - selfClosingTags, - getPropKey, -} from '@component-elements/shared'; +import { CustomElement, parseHtml, selfClosingTags, getPropKey } from '@component-elements/shared'; /* ----------------------------------- * - * parseHtml + * parseChildren * * -------------------------------- */ -function parseHtml(this: CustomElement): ComponentFactory<{}> { - const dom = getDocument(this.innerHTML); +function parseChildren(this: CustomElement): ComponentFactory<{}> { + const children = parseHtml(this.innerHTML); - if (!dom) { + if (!children.length) { return void 0; } - const result = convertToVDom.call(this, dom); + const result = convertToVDom.call(this, children); return () => result; } @@ -31,27 +25,19 @@ function parseHtml(this: CustomElement): ComponentFactory<{}> { * * -------------------------------- */ -function convertToVDom(this: CustomElement, node: Element) { - if (node.nodeType === 3) { - return node.textContent?.trim() || ''; +function convertToVDom(this: CustomElement, [nodeName, {slot, ...props}, children]: any) { + if(typeof children === 'string') { + return children.trim(); } - if (node.nodeType !== 1) { + if(nodeName === 'script') { return null; } - const nodeName = String(node.nodeName).toLowerCase(); - const childNodes = Array.from(node.childNodes); - - const children = () => childNodes.map((child) => convertToVDom.call(this, child)); - const { slot, ...props } = getAttributeObject(node.attributes); - - if (nodeName === 'script') { - return null; - } + const childNodes = () => children.map((child) => convertToVDom.call(this, child)); if (nodeName === 'body') { - return h(Fragment, {}, children()); + return h(Fragment, {}, childNodes()); } if (selfClosingTags.includes(nodeName)) { @@ -59,12 +45,12 @@ function convertToVDom(this: CustomElement, node: Element) { } if (slot) { - this.__slots[getPropKey(slot)] = getSlotChildren(children()); + this.__slots[getPropKey(slot)] = getSlotChildren(childNodes()); return null; } - return h(nodeName, props, children()); + return h(nodeName, props, childNodes()); } /* ----------------------------------- @@ -89,4 +75,4 @@ function getSlotChildren(children: JSX.Element[]) { * * -------------------------------- */ -export { parseHtml }; +export { parseChildren }; diff --git a/packages/preactement/tests/parse.spec.ts b/packages/preactement/tests/parse.spec.ts index c1403cf..73ea910 100644 --- a/packages/preactement/tests/parse.spec.ts +++ b/packages/preactement/tests/parse.spec.ts @@ -1,6 +1,6 @@ import { h } from 'preact'; import { mount } from 'enzyme'; -import { parseHtml } from '../src/parse'; +import { parseChildren } from '../src/parse'; /* ----------------------------------- * @@ -20,6 +20,15 @@ const testScript = ``; * -------------------------------- */ describe('parse', () => { + describe('parseChildren', () => { + it('correctly converts an HTML string into a VDom tree', () => { + const result = parseChildren.call({ innerHTML: testHtml }); + const instance = mount(h(result, {}) as any); + + expect(instance.find('h1').text()).toEqual(testHeading); + }); + }) + /* describe('parseHtml()', () => { it('should correctly handle misformed html', () => { const testText = 'testText'; @@ -77,4 +86,5 @@ describe('parse', () => { }); }); }); + */ }); diff --git a/packages/shared/tests/parse.spec.ts b/packages/shared/tests/parse.spec.ts index 12c033e..fdb9074 100644 --- a/packages/shared/tests/parse.spec.ts +++ b/packages/shared/tests/parse.spec.ts @@ -54,11 +54,9 @@ describe('parse', () => { }); describe('parseHtml', () => { - it('correctly converts a DOM structure to data', () => { + it('correctly converts a DOM structure to multidimensional array', () => { const result = parseHtml(testHtml); - console.log(result); - expect(result).toEqual([null, {}, [ ['h1', {}, [[null, {}, testHeading]]], ['br', {}, []], From 3f31eba4b53a714668d264817efffa8f6a228398 Mon Sep 17 00:00:00 2001 From: James Hill Date: Thu, 28 Apr 2022 16:15:56 +0100 Subject: [PATCH 3/5] Fixed unit tests for preactement parse --- packages/preactement/src/parse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preactement/src/parse.ts b/packages/preactement/src/parse.ts index 3c6f462..fe0d1e5 100644 --- a/packages/preactement/src/parse.ts +++ b/packages/preactement/src/parse.ts @@ -36,7 +36,7 @@ function convertToVDom(this: CustomElement, [nodeName, {slot, ...props}, childre const childNodes = () => children.map((child) => convertToVDom.call(this, child)); - if (nodeName === 'body') { + if (nodeName === null) { return h(Fragment, {}, childNodes()); } From bd63847c0d4ce4d11c736fe9ec4d26d615985cc6 Mon Sep 17 00:00:00 2001 From: James Hill Date: Thu, 28 Apr 2022 16:44:44 +0100 Subject: [PATCH 4/5] Finished moving dom conversion logic into shared package --- packages/preactement/src/parse.ts | 6 ++- packages/preactement/tests/parse.spec.ts | 24 +++++------- packages/reactement/src/define.ts | 4 +- packages/reactement/src/parse.ts | 48 +++++++++--------------- packages/reactement/tests/parse.spec.ts | 14 +++---- packages/shared/jest.config.js | 2 +- packages/shared/src/parse.ts | 8 ++-- 7 files changed, 45 insertions(+), 61 deletions(-) diff --git a/packages/preactement/src/parse.ts b/packages/preactement/src/parse.ts index fe0d1e5..41a2c09 100644 --- a/packages/preactement/src/parse.ts +++ b/packages/preactement/src/parse.ts @@ -30,11 +30,13 @@ function convertToVDom(this: CustomElement, [nodeName, {slot, ...props}, childre return children.trim(); } - if(nodeName === 'script') { + if(nodeName === void 0) { return null; } - const childNodes = () => children.map((child) => convertToVDom.call(this, child)); + const childNodes = () => children.map((child) => + child.length ? convertToVDom.call(this, child) : void 0 + ); if (nodeName === null) { return h(Fragment, {}, childNodes()); diff --git a/packages/preactement/tests/parse.spec.ts b/packages/preactement/tests/parse.spec.ts index 73ea910..b546a2e 100644 --- a/packages/preactement/tests/parse.spec.ts +++ b/packages/preactement/tests/parse.spec.ts @@ -1,6 +1,7 @@ import { h } from 'preact'; import { mount } from 'enzyme'; import { parseChildren } from '../src/parse'; +import { parseHtml } from '@component-elements/shared'; /* ----------------------------------- * @@ -27,26 +28,24 @@ describe('parse', () => { expect(instance.find('h1').text()).toEqual(testHeading); }); - }) - /* - describe('parseHtml()', () => { + it('should correctly handle misformed html', () => { const testText = 'testText'; - const result = parseHtml.call({ innerHTML: `

${testText}` }); + const result = parseChildren.call({ innerHTML: `

${testText}` }); const instance = mount(h(result, {}) as any); expect(instance.html()).toEqual(`

${testText}

`); }); it('handles text values witin custom element', () => { - const result = parseHtml.call({ innerHTML: testHeading }); + const result = parseChildren.call({ innerHTML: testHeading }); const instance = mount(h(result, {}) as any); expect(instance.text()).toEqual(testHeading); }); it('handles whitespace within custom element', () => { - const result = parseHtml.call({ innerHTML: testWhitespace }); + const result = parseChildren.call({ innerHTML: testWhitespace }); const instance = mount(h(result, {}) as any); expect(instance.text()).toEqual(''); @@ -54,17 +53,13 @@ describe('parse', () => { }); it('removes script blocks for security', () => { - const result = parseHtml.call({ innerHTML: testScript }); - const instance = mount(h(result, {}) as any); + const result = parseChildren.call({ innerHTML: testScript }); - expect(instance.text()).toEqual(''); - }); + console.log(parseHtml(testScript)); - it('correctly converts an HTML string into a VDom tree', () => { - const result = parseHtml.call({ innerHTML: testHtml }); const instance = mount(h(result, {}) as any); - expect(instance.find('h1').text()).toEqual(testHeading); + expect(instance.text()).toEqual(''); }); describe('slots', () => { @@ -78,7 +73,7 @@ describe('parse', () => { const headingHtml = `

${testHeading}

`; const testHtml = `
${headingHtml}${slotHtml}
`; - const result = parseHtml.call({ innerHTML: testHtml, __slots: slots }); + const result = parseChildren.call({ innerHTML: testHtml, __slots: slots }); const instance = mount(h(result, {}) as any); expect(instance.html()).toEqual(`
${headingHtml}
`); @@ -86,5 +81,4 @@ describe('parse', () => { }); }); }); - */ }); diff --git a/packages/reactement/src/define.ts b/packages/reactement/src/define.ts index 345285f..f0a61b2 100644 --- a/packages/reactement/src/define.ts +++ b/packages/reactement/src/define.ts @@ -11,7 +11,7 @@ import { getElementAttributes, getAsyncComponent, } from '@component-elements/shared'; -import { parseHtml } from './parse'; +import { parseChildren } from './parse'; import { IOptions, ComponentFunction } from './model'; /* ----------------------------------- @@ -132,7 +132,7 @@ function onConnected(this: CustomElement) { let children = this.__children; if (!this.__mounted && !this.hasAttribute('server')) { - children = createElement(parseHtml.call(this), {}); + children = createElement(parseChildren.call(this), {}); } this.__properties = { ...this.__slots, ...data, ...attributes }; diff --git a/packages/reactement/src/parse.ts b/packages/reactement/src/parse.ts index 4f0ded8..02b54dd 100644 --- a/packages/reactement/src/parse.ts +++ b/packages/reactement/src/parse.ts @@ -1,26 +1,20 @@ import React, { createElement, ComponentFactory, Fragment } from 'react'; -import { - CustomElement, - getDocument, - getAttributeObject, - selfClosingTags, - getPropKey -} from '@component-elements/shared'; +import { CustomElement, parseHtml, selfClosingTags, getPropKey } from '@component-elements/shared'; /* ----------------------------------- * - * parseHtml + * parseChildren * * -------------------------------- */ -function parseHtml(this: CustomElement): ComponentFactory<{}, any> { - const dom = getDocument(this.innerHTML); +function parseChildren(this: CustomElement): ComponentFactory<{}, any> { + const children = parseHtml(this.innerHTML); - if (!dom) { + if (!children.length) { return void 0; } - const result = convertToVDom.call(this, dom); + const result = convertToVDom.call(this, children); return () => result; } @@ -31,27 +25,21 @@ function parseHtml(this: CustomElement): ComponentFactory<{}, any> { * * -------------------------------- */ -function convertToVDom(this: CustomElement, node: Element) { - if (node.nodeType === 3) { - return node.textContent?.trim() || ''; +function convertToVDom(this: CustomElement, [nodeName, {slot, ...props}, children]: any) { + if(typeof children === 'string') { + return children.trim(); } - if (node.nodeType !== 1) { + if(nodeName === void 0) { return null; } - const nodeName = String(node.nodeName).toLowerCase(); - const childNodes = Array.from(node.childNodes); + const childNodes = () => children.map((child) => + child.length ? convertToVDom.call(this, child) : void 0 + ); - const children = () => childNodes.map((child) => convertToVDom.call(this, child)); - const { slot, ...props } = getAttributeObject(node.attributes); - - if (nodeName === 'script') { - return null; - } - - if (nodeName === 'body') { - return createElement(Fragment, {}, children()); + if (nodeName === null) { + return createElement(Fragment, {}, childNodes()); } if (selfClosingTags.includes(nodeName)) { @@ -59,12 +47,12 @@ function convertToVDom(this: CustomElement, node: Element) { } if (slot) { - this.__slots[getPropKey(slot)] = getSlotChildren(children()); + this.__slots[getPropKey(slot)] = getSlotChildren(childNodes()); return null; } - return createElement(nodeName, { ...props, key: Math.random() }, children()); + return createElement(nodeName, props, childNodes()); } /* ----------------------------------- @@ -89,4 +77,4 @@ function getSlotChildren(children: JSX.Element[]) { * * -------------------------------- */ -export { parseHtml }; +export { parseChildren }; diff --git a/packages/reactement/tests/parse.spec.ts b/packages/reactement/tests/parse.spec.ts index 337e514..5dba788 100644 --- a/packages/reactement/tests/parse.spec.ts +++ b/packages/reactement/tests/parse.spec.ts @@ -1,6 +1,6 @@ import React, { createElement } from 'react'; import { mount } from 'enzyme'; -import { parseHtml } from '../src/parse'; +import { parseChildren } from '../src/parse'; /* ----------------------------------- * @@ -24,21 +24,21 @@ describe('parse', () => { describe('parseHtml()', () => { it('should correctly handle misformed html', () => { const testText = 'testText'; - const result = parseHtml.call({ innerHTML: `

${testText}` }); + const result = parseChildren.call({ innerHTML: `

${testText}` }); const instance = mount(createElement(result, {}) as any); expect(instance.html()).toEqual(`

${testText}

`); }); it('handles text values witin custom element', () => { - const result = parseHtml.call({ innerHTML: testHeading }); + const result = parseChildren.call({ innerHTML: testHeading }); const instance = mount(createElement(result, {}) as any); expect(instance.text()).toEqual(testHeading); }); it('handles whitespace within custom element', () => { - const result = parseHtml.call({ innerHTML: testWhitespace }); + const result = parseChildren.call({ innerHTML: testWhitespace }); const instance = mount(createElement(result, {}) as any); expect(instance.text()).toEqual(''); @@ -46,14 +46,14 @@ describe('parse', () => { }); it('removes script blocks for security', () => { - const result = parseHtml.call({ innerHTML: testScript }); + const result = parseChildren.call({ innerHTML: testScript }); const instance = mount(createElement(result, {}) as any); expect(instance.text()).toEqual(''); }); it('correctly converts an HTML string into a VDom tree', () => { - const result = parseHtml.call({ innerHTML: testHtml }); + const result = parseChildren.call({ innerHTML: testHtml }); const instance = mount(createElement(result, {}) as any); expect(instance.find('h1').text()).toEqual(testHeading); @@ -68,7 +68,7 @@ describe('parse', () => { const headingHtml = `

${testHeading}

`; const testHtml = `
${headingHtml}${slotHtml}
`; - const result = parseHtml.call({ innerHTML: testHtml, __slots: slots }); + const result = parseChildren.call({ innerHTML: testHtml, __slots: slots }); const instance = mount(createElement(result, {}) as any); expect(instance.html()).toEqual(`
${headingHtml}
`); diff --git a/packages/shared/jest.config.js b/packages/shared/jest.config.js index be674ab..76210c1 100644 --- a/packages/shared/jest.config.js +++ b/packages/shared/jest.config.js @@ -15,7 +15,7 @@ module.exports = { coverageThreshold: { global: { statements: 84, - branches: 73, + branches: 69, functions: 80, lines: 82, }, diff --git a/packages/shared/src/parse.ts b/packages/shared/src/parse.ts index 7a4ff12..c2e8ff6 100644 --- a/packages/shared/src/parse.ts +++ b/packages/shared/src/parse.ts @@ -76,13 +76,13 @@ function domToArray(node: Element) { return [null, {}, node.textContent?.trim() || '']; } - if (node.nodeType !== 1) { - return []; - } - const nodeName = String(node.nodeName).toLowerCase(); const childNodes = Array.from(node.childNodes); + if (nodeName === 'script' || node.nodeType !== 1) { + return []; + } + const children = () => childNodes.map((child: Element) => domToArray(child)); const props = getAttributeObject(node.attributes); From 8c3ac5518909d02d2a0647afbf3982dc5939764c Mon Sep 17 00:00:00 2001 From: James Hill Date: Thu, 28 Apr 2022 16:50:10 +0100 Subject: [PATCH 5/5] Bumped code coverage thresholds for shared --- packages/shared/jest.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shared/jest.config.js b/packages/shared/jest.config.js index 76210c1..65f9584 100644 --- a/packages/shared/jest.config.js +++ b/packages/shared/jest.config.js @@ -14,10 +14,10 @@ module.exports = { coveragePathIgnorePatterns: ['/node_modules/', '(.*).d.ts'], coverageThreshold: { global: { - statements: 84, + statements: 91, branches: 69, - functions: 80, - lines: 82, + functions: 94, + lines: 90, }, }, transform: {