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..41a2c09 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,21 @@ 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 === 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 h(Fragment, {}, children()); + if (nodeName === null) { + return h(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 h(nodeName, props, children()); + return h(nodeName, props, childNodes()); } /* ----------------------------------- @@ -89,4 +77,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..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 { parseHtml } from '../src/parse'; +import { parseChildren } from '../src/parse'; +import { parseHtml } from '@component-elements/shared'; /* ----------------------------------- * @@ -20,24 +21,31 @@ const testScript = ``; * -------------------------------- */ describe('parse', () => { - describe('parseHtml()', () => { + 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); + }); + 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(''); @@ -45,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', () => { @@ -69,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}
`); 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..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, - branches: 73, - functions: 80, - lines: 82, + statements: 91, + branches: 69, + functions: 94, + lines: 90, }, }, transform: { diff --git a/packages/shared/src/parse.ts b/packages/shared/src/parse.ts index 4932cf5..c2e8ff6 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() || '']; + } + + 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); + + 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..fdb9074 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,23 @@ describe('parse', () => { }); }); + describe('parseHtml', () => { + it('correctly converts a DOM structure to multidimensional array', () => { + const result = parseHtml(testHtml); + + 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';