Skip to content

Commit 9d745fa

Browse files
New: Use browser DOMParser and drop external dependencies
1 parent cc76537 commit 9d745fa

File tree

9 files changed

+1703
-1632
lines changed

9 files changed

+1703
-1632
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: 14 additions & 14 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',
@@ -63,36 +63,36 @@ function transformAttributes(attributes?: Attributes): Attributes {
6363
return transformedAttributes
6464
}
6565

66-
function transformChildren(children?: Element[]) {
66+
function transformChildren(children?: ASTElement[]) {
6767
return children || []
6868
}
6969

70-
export function transform(element: Element) {
70+
export function transform(element: ASTElement) {
7171
return {
7272
...element,
73-
attribs: transformAttributes(element.attribs),
73+
attributes: transformAttributes(element.attributes),
7474
children: transformChildren(element.children),
7575
}
7676
}
7777

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,
78+
export function renderElement(element: ASTElement): React.ReactNode {
79+
if (element.type === NodeType.TEXT_NODE) {
80+
return element.value
81+
} else if (element.type === NodeType.ELEMENT_NODE) {
82+
const children = element.value ? [
83+
element.value,
8484
...renderElements(element.children),
8585
] : renderElements(element.children)
86-
return React.createElement(element.name, element.attribs, children)
86+
return React.createElement(element.name, element.attributes, children)
8787
}
8888

8989
return null
9090
}
9191

92-
export function renderElements(ast: Element[]): React.ReactNode [] {
92+
export function renderElements(ast: ASTElement[]): React.ReactNode[] {
9393
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') {
94+
// Only keep text and tags. Remove everything else
95+
if (element.type === NodeType.ELEMENT_NODE || element.type === NodeType.TEXT_NODE) {
9696
elements.push(renderElement(transform(element)))
9797
}
9898
return elements

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
})

tests/react.spec.ts

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

4+
import { NodeType } from '../src/ast'
45
import { renderElement, renderElements, transform } from '../src/react'
56

67
describe('React', () => {
78
describe('#transform', () => {
89
it('should remove event handler from attributes', () => {
910
const element = {
10-
attribs: {
11+
attributes: {
1112
onclick: 'wazaaa',
1213
},
1314
name: 'button',
14-
type: 'tag',
15+
type: NodeType.ELEMENT_NODE,
16+
value: null,
1517
}
1618

1719
expect(transform(element)).toEqual({
18-
attribs: {},
20+
attributes: {},
1921
children: [],
2022
name: 'button',
21-
type: 'tag',
23+
type: NodeType.ELEMENT_NODE,
24+
value: null,
2225
})
2326
})
2427

2528
it('should rename attributes to match react syntax', () => {
2629
const element = {
27-
attribs: {
30+
attributes: {
2831
class: 'wazaaa',
2932
for: 'test',
3033
},
3134
name: 'button',
32-
type: 'tag',
35+
type: NodeType.ELEMENT_NODE,
36+
value: null,
3337
}
3438

3539
expect(transform(element)).toEqual({
36-
attribs: {
40+
attributes: {
3741
className: 'wazaaa',
3842
htmlFor: 'test',
3943
},
4044
children: [],
4145
name: 'button',
42-
type: 'tag',
46+
type: NodeType.ELEMENT_NODE,
47+
value: null,
4348
})
4449
})
4550
})
4651

4752
describe('#renderElement', () => {
4853
it('should render a text element', () => {
4954
const element = {
50-
attribs: {},
51-
data: 'Test',
52-
type: 'text',
55+
name: '#text',
56+
type: NodeType.TEXT_NODE,
57+
value: 'Test',
5358
}
5459

5560
expect(renderElement(element)).toEqual('Test')
5661
})
5762

5863
it('should render a tag element', () => {
5964
const element = {
60-
attribs: {},
65+
attributes: {},
6166
children: [],
62-
data: 'Link',
6367
name: 'a',
64-
type: 'tag',
68+
type: NodeType.ELEMENT_NODE,
69+
value: 'Link',
6570
}
6671
const link = shallow(React.createElement('div', null, renderElement(element)))
6772
expect(link.text()).toEqual('Link')
6873
})
6974

7075
it('should render a tag element with attributes', () => {
7176
const element = {
72-
attribs: {
77+
attributes: {
7378
href: '#',
7479
},
7580
children: [],
7681
name: 'a',
77-
type: 'tag',
82+
type: NodeType.ELEMENT_NODE,
83+
value: null,
7884
}
7985
const wrapper = shallow(React.createElement('div', null, renderElement(element)))
8086
const link = wrapper.find('a')
@@ -85,15 +91,17 @@ describe('React', () => {
8591
const element = {
8692
children: [{
8793
children: [{
88-
attribs: {},
89-
data: 'text',
90-
type: 'text',
94+
name: '#text',
95+
type: NodeType.TEXT_NODE,
96+
value: 'text',
9197
}],
9298
name: 'b',
93-
type: 'tag',
99+
type: NodeType.ELEMENT_NODE,
100+
value: null,
94101
}],
95102
name: 'em',
96-
type: 'tag',
103+
type: NodeType.ELEMENT_NODE,
104+
value: null,
97105
}
98106
const nested = render(React.createElement('div', null, renderElement(element)))
99107

@@ -110,7 +118,8 @@ describe('React', () => {
110118
it('should ignore unknown type', () => {
111119
const element = {
112120
name: 'em',
113-
type: '',
121+
type: -1,
122+
value: null,
114123
}
115124
expect(renderElement(element)).toBeNull()
116125
})
@@ -119,11 +128,9 @@ describe('React', () => {
119128
describe('#renderElements', () => {
120129
it('should only render text and tag elements', () => {
121130
const elements = [{
122-
attribs: {},
123-
children: [],
124-
data: 'Link',
125131
name: 'a',
126-
type: '',
132+
type: -1,
133+
value: 'Link',
127134
}]
128135
expect(renderElements(elements)).toEqual([])
129136
})

0 commit comments

Comments
 (0)