Skip to content

Commit 9aea82f

Browse files
Merge pull request #2 from thomasthiebaud/reduce-bundle-size
Reduce bundle size
2 parents cc76537 + dd967fc commit 9aea82f

File tree

10 files changed

+1717
-1636
lines changed

10 files changed

+1717
-1636
lines changed

package-lock.json

Lines changed: 1565 additions & 1550 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,7 @@
4646
"tslint-react": "3.6.0",
4747
"typescript": "2.9.2"
4848
},
49-
"dependencies": {
50-
"domhandler": "2.4.2",
51-
"htmlparser2": "3.9.2"
52-
},
49+
"dependencies": {},
5350
"peerDependencies": {
5451
"react": "^16.2.0"
5552
}

src/ast.ts

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,77 @@
1-
import * as DomHandler from 'domhandler'
2-
import * as Parser from 'htmlparser2/lib/Parser'
3-
41
export interface Attributes { [keyof: string]: string }
52

6-
export interface Element {
7-
attribs?: Attributes
8-
children?: Element[]
9-
data?: string
10-
type: string
11-
name?: string
3+
export interface ASTElement {
4+
attributes?: Attributes
5+
children?: ASTElement[]
6+
name: string | null
7+
type: NodeType
8+
value: string | null
9+
}
10+
11+
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
12+
export enum NodeType {
13+
ELEMENT_NODE = 1,
14+
TEXT_NODE = 3,
15+
PROCESSING_INSTRUCTION_NODE = 7,
16+
COMMENT_NODE = 8,
17+
DOCUMENT_NODE = 9,
18+
DOCUMENT_TYPE_NODE = 10,
19+
DOCUMENT_FRAGMENT_NODE = 11,
20+
}
21+
22+
function getAttributes(elementAttributes: NamedNodeMap) {
23+
const attributes: Attributes = {}
24+
for (let i = 0; i < elementAttributes.length; i++) {
25+
const attribute = elementAttributes.item(i)
26+
attributes[attribute.nodeName] = attribute.nodeValue
27+
}
28+
return attributes
29+
}
30+
31+
function nodeToElement(node: Node): ASTElement {
32+
if (node.nodeType === NodeType.ELEMENT_NODE) {
33+
const element = node as Element
34+
return {
35+
attributes: getAttributes(element.attributes),
36+
children: nodesToElement(element.childNodes),
37+
name: element.nodeName,
38+
type: element.nodeType,
39+
value: element.nodeValue,
40+
}
41+
} else if (node.nodeType === NodeType.TEXT_NODE) {
42+
return {
43+
children: nodesToElement(node.childNodes),
44+
name: node.nodeName,
45+
type: node.nodeType,
46+
value: node.nodeValue,
47+
}
48+
}
49+
50+
return null
1251
}
1352

14-
export interface AST extends Array<Element> {}
53+
function nodesToElement(nodes: NodeListOf<Node & ChildNode>): ASTElement[] {
54+
const elements: ASTElement[] = []
55+
56+
for (let i = 0; i < nodes.length; i++) {
57+
const node = nodes.item(i)
58+
const element = nodeToElement(node)
59+
60+
if (element) {
61+
elements.push(element)
62+
}
63+
}
64+
return elements
65+
}
1566

16-
export default function getAst(html: string): AST {
67+
export default function getAst(html: string): ASTElement[] {
1768
if (typeof html !== 'string') {
1869
throw new TypeError('First argument must be a string.')
1970
}
20-
const handler = new DomHandler()
21-
const parser = new Parser(handler, {
22-
decodeEntities: true,
23-
lowerCaseAttributeNames: false,
24-
lowerCaseTags: false,
25-
recognizeSelfClosing: true,
26-
})
27-
parser.write(html)
28-
parser.end()
29-
return handler.dom
71+
72+
const parser = new DOMParser()
73+
const document = parser.parseFromString(`<htmldomtoreactroot>${html}</htmldomtoreactroot>`, 'application/xml')
74+
75+
const astFromRoot = nodesToElement(document.childNodes)
76+
return astFromRoot[0].children
3077
}

src/domhandler.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/htmlparser2.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import getAst from './ast'
2-
import { renderElements, transform } from './react'
2+
import { renderElements } from './react'
33

44
export function parse(html: string): React.ReactNode[] {
55
const ast = getAst(html)

src/react.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react'
2-
import { Attributes, Element } from './ast'
2+
import { ASTElement, Attributes, NodeType } from './ast'
33

44
const reactAttributesMap: Attributes = {
55
acceptcharset: 'acceptCharset',
@@ -56,43 +56,47 @@ function transformAttributes(attributes?: Attributes): Attributes {
5656

5757
const transformedAttributes: Attributes = {}
5858
Object.keys(attributes).forEach((key) => {
59-
if (!attributes[key].startsWith('on') && reactAttributesMap[key]) {
60-
transformedAttributes[reactAttributesMap[key]] = attributes[key]
59+
if (!key.startsWith('on')) {
60+
if (reactAttributesMap[key]) {
61+
transformedAttributes[reactAttributesMap[key]] = attributes[key]
62+
} else {
63+
transformedAttributes[key] = attributes[key]
64+
}
6165
}
6266
})
6367
return transformedAttributes
6468
}
6569

66-
function transformChildren(children?: Element[]) {
70+
function transformChildren(children?: ASTElement[]) {
6771
return children || []
6872
}
6973

70-
export function transform(element: Element) {
74+
export function transform(element: ASTElement) {
7175
return {
7276
...element,
73-
attribs: transformAttributes(element.attribs),
77+
attributes: transformAttributes(element.attributes),
7478
children: transformChildren(element.children),
7579
}
7680
}
7781

78-
export function renderElement(element: Element): React.ReactNode {
79-
if (element.type === 'text') {
80-
return element.data
81-
} else if (element.type === 'tag') {
82-
const children = element.data ? [
83-
element.data,
82+
export function renderElement(element: ASTElement): React.ReactNode {
83+
if (element.type === NodeType.TEXT_NODE) {
84+
return element.value
85+
} else if (element.type === NodeType.ELEMENT_NODE) {
86+
const children = element.value ? [
87+
element.value,
8488
...renderElements(element.children),
8589
] : renderElements(element.children)
86-
return React.createElement(element.name, element.attribs, children)
90+
return React.createElement(element.name, element.attributes, children)
8791
}
8892

8993
return null
9094
}
9195

92-
export function renderElements(ast: Element[]): React.ReactNode [] {
96+
export function renderElements(ast: ASTElement[]): React.ReactNode[] {
9397
return ast.reduce((elements, element) => {
94-
// Only keep text and tags. Remove everything else like comments, scritps,...
95-
if (element.type === 'tag' || element.type === 'text') {
98+
// Only keep text and tags. Remove everything else
99+
if (element.type === NodeType.ELEMENT_NODE || element.type === NodeType.TEXT_NODE) {
96100
elements.push(renderElement(transform(element)))
97101
}
98102
return elements

tests/api.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, shallow } from 'enzyme'
1+
import { render } from 'enzyme'
22
import * as React from 'react'
33

44
import * as htmldomToReact from '../src/index'
@@ -10,7 +10,7 @@ describe('Public API', () => {
1010

1111
describe('#parse', () => {
1212
it('should convert a string to an array of react nodes', () => {
13-
const elements = htmldomToReact.parse('<em><b>It\' is working</b></em>')
13+
const elements = htmldomToReact.parse('<em key="1"><b key="2">It\' is working</b></em>')
1414
const wrapper = render(React.createElement('div', null, elements))
1515
expect(wrapper.text()).toEqual('It\' is working')
1616
expect(wrapper.find('em')).toHaveLength(1)

tests/ast.spec.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import getAst from '../src/ast'
1+
import getAst, { NodeType } from '../src/ast'
22

33
describe('AST', () => {
44
describe('#getAst', () => {
@@ -13,37 +13,44 @@ describe('AST', () => {
1313
it('should convert a string to an ast', () => {
1414
const ast = getAst('This is a test')
1515
expect(ast).toHaveLength(1)
16-
expect(ast[0].data).toEqual('This is a test')
17-
expect(ast[0].type).toEqual('text')
16+
expect(ast[0].value).toEqual('This is a test')
17+
expect(ast[0].type).toEqual(NodeType.TEXT_NODE)
1818
})
1919

2020
it('should correctly handle self closing tag', () => {
2121
const ast = getAst('This is <tag/> a test')
2222
expect(ast).toHaveLength(3)
2323

24-
expect(ast[0].data).toEqual('This is ')
25-
expect(ast[0].type).toEqual('text')
24+
expect(ast[0].value).toEqual('This is ')
25+
expect(ast[0].type).toEqual(NodeType.TEXT_NODE)
2626

2727
expect(ast[1].name).toEqual('tag')
28-
expect(ast[1].type).toEqual('tag')
28+
expect(ast[1].type).toEqual(NodeType.ELEMENT_NODE)
2929

30-
expect(ast[2].data).toEqual(' a test')
31-
expect(ast[2].type).toEqual('text')
30+
expect(ast[2].value).toEqual(' a test')
31+
expect(ast[2].type).toEqual(NodeType.TEXT_NODE)
3232
})
3333

3434
it('should not lowercase tag and attributes names', () => {
3535
const ast = getAst('This is <TAG ATT="test"/> a test')
3636
expect(ast).toHaveLength(3)
3737

38-
expect(ast[0].data).toEqual('This is ')
39-
expect(ast[0].type).toEqual('text')
38+
expect(ast[0].value).toEqual('This is ')
39+
expect(ast[0].type).toEqual(NodeType.TEXT_NODE)
4040

4141
expect(ast[1].name).toEqual('TAG')
42-
expect(ast[1].attribs).toEqual({ ATT: 'test' })
43-
expect(ast[1].type).toEqual('tag')
42+
expect(ast[1].attributes).toEqual({ ATT: 'test' })
43+
expect(ast[1].type).toEqual(NodeType.ELEMENT_NODE)
4444

45-
expect(ast[2].data).toEqual(' a test')
46-
expect(ast[2].type).toEqual('text')
45+
expect(ast[2].value).toEqual(' a test')
46+
expect(ast[2].type).toEqual(NodeType.TEXT_NODE)
47+
})
48+
49+
it('should ignore comments', () => {
50+
const ast = getAst('This is a <!-- comment -->')
51+
expect(ast).toHaveLength(1)
52+
expect(ast[0].value).toEqual('This is a ')
53+
expect(ast[0].type).toEqual(NodeType.TEXT_NODE)
4754
})
4855
})
4956
})

0 commit comments

Comments
 (0)