├── .babelrc
├── .gitignore
├── README.md
├── index.html
├── package.json
├── rollup.config.js
└── src
├── generator.js
├── index.js
├── lexer.js
├── mock-data.js
└── parser.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["es2015", {"modules": false}]
4 | ],
5 | "plugins": ["external-helpers"]
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Toy HTML Compiler
2 | 一个玩具级的 HTML 转虚拟 DOM 编译器
3 |
4 |
5 | ## 特性
6 | * 解析 HTML 字符串到 JS 虚拟 DOM 对象
7 | * 50 行内的 Parser
8 |
9 | ### 支持
10 | * 类似 `
123
456` 的任意多个标签并列
11 | * 类似 `123
` 的任意层嵌套标签
12 |
13 | ### 不支持
14 | * 类似 `` 的自闭合标签
15 | * 类似 `123456
` 的混合嵌套
16 |
17 |
18 | ## License
19 | MIT
20 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Parser Demo
5 |
10 |
11 |
12 |
13 |
21 |
22 |
Virtual DOM will be logged to console.
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "babel-core": "^6.0.0",
4 | "babel-plugin-external-helpers": "^6.0.0",
5 | "babel-preset-es2015": "^6.0.0",
6 | "cross-env": "^3.0.0",
7 | "http-server": "^0.10.0",
8 | "rollup": "^0.41.4",
9 | "rollup-plugin-babel": "^2.7.1",
10 | "rollup-plugin-uglify": "^1.0.1",
11 | "rollup-watch": "^3.2.2"
12 | },
13 | "name": "toy-html-parser",
14 | "description": "toy html parser",
15 | "version": "0.1.0",
16 | "main": "src/index.js",
17 | "scripts": {
18 | "dev": "cross-env NODE_ENV=dev rollup -c --watch",
19 | "build": "cross-env NODE_ENV=production rollup -c",
20 | "example": "http-server . -s -p 10008"
21 | },
22 | "keywords": [
23 | "compiler"
24 | ],
25 | "author": "doodlewind",
26 | "license": "MIT"
27 | }
28 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel'
2 | import uglify from 'rollup-plugin-uglify'
3 |
4 | const plugins = [babel()]
5 | if (process.env.NODE_ENV === 'production') plugins.push(uglify())
6 |
7 | export default({
8 | entry: 'src/index.js',
9 | dest: 'dist/bundle.js',
10 | format: 'iife',
11 | plugins
12 | })
13 |
--------------------------------------------------------------------------------
/src/generator.js:
--------------------------------------------------------------------------------
1 | // 修改匹配所用正则
2 | function trim (str) {
3 | return str.replace(/^<|>$/g, '').split(' ')[0]
4 | }
5 | function getClassName (str) {
6 | const re = /class='(\w)+'/
7 | if (re.test(str)) {
8 | return str.match(re)[0].replace("class='", '').replace("'", '')
9 | } else return null
10 | }
11 |
12 | // 添加 className 至渲染属性中
13 | function renderNode ($target, nodes) {
14 | nodes.forEach(node => {
15 | const className = getClassName(node.type)
16 | let newNode = document.createElement(trim(node.type))
17 | if (className) newNode.classList.add(className)
18 | if (!node.val) newNode = renderNode(newNode, node.children)
19 | else newNode.innerText = node.val
20 | $target.appendChild(newNode)
21 | })
22 | return $target
23 | }
24 |
25 | function render (dom, targetId) {
26 | let target = document.getElementById(targetId)
27 | target.innerHTML = ''
28 | renderNode(target, dom.children)
29 | }
30 |
31 | export default {
32 | render,
33 | initBrowser (lexer, parser) {
34 | document.getElementById('compile').addEventListener('click', () => {
35 | let template = document.getElementById('html-template').value
36 | try {
37 | let tokens = lexer.lex(template)
38 | let ast = parser.parse(tokens)
39 | render(ast, 'target')
40 | } catch (e) { window.alert(e) }
41 | }, false)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import lexer from './lexer'
2 | import parser from './parser'
3 | import generator from './generator'
4 | // import mockData from './mock-data'
5 |
6 | generator.initBrowser(lexer, parser)
7 |
--------------------------------------------------------------------------------
/src/lexer.js:
--------------------------------------------------------------------------------
1 | const TagClose = new RegExp(/^<\/[\w]+>/)
2 | // 匹配时需优化 TagOpen 表达式
3 | const TagOpen = new RegExp(/^<[\w]+(\s)?[^>]+>/)
4 | const Value = new RegExp(/^[^<]+/)
5 |
6 | function trim (str) {
7 | return str.replace(/^\s+|\s+$/, '')
8 | }
9 |
10 | function getToken (str) {
11 | if (str.match(TagClose)) {
12 | return { type: 'TagClose', val: str.match(TagClose)[0] }
13 | } else if (str.match(TagOpen)) {
14 | return { type: 'TagOpen', val: str.match(TagOpen)[0] }
15 | } else if (str.match(Value)) {
16 | return { type: 'Value', val: str.match(Value)[0] }
17 | }
18 | throw Error('LexicalError')
19 | }
20 |
21 | export default {
22 | lex (template) {
23 | let tokens = []
24 | let token
25 | while (template.length > 0) {
26 | template = trim(template)
27 | token = getToken(template)
28 | template = template.substr(token.val.length)
29 | token.val = trim(token.val)
30 | tokens.push(token)
31 | }
32 | return tokens
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/mock-data.js:
--------------------------------------------------------------------------------
1 | export default {
2 | template: `
3 | hello
4 |
5 |
6 | world
7 |
8 | !
9 | `,
10 | tokens: [
11 | { type: 'TagOpen', val: '' },
12 | { type: 'Value', val: 'hello' },
13 | { type: 'TagClose', val: '
' },
14 |
15 | { type: 'TagOpen', val: '' },
16 |
17 | { type: 'TagOpen', val: '
' },
18 | { type: 'TagOpen', val: '' },
19 | { type: 'Value', val: 'world' },
20 | { type: 'TagClose', val: '' },
21 | { type: 'TagClose', val: '
' },
22 |
23 | { type: 'TagOpen', val: '' },
24 | { type: 'Value', val: '!' },
25 | { type: 'TagClose', val: '' },
26 |
27 | { type: 'TagClose', val: '' }
28 | ],
29 | dom: {
30 | type: 'html',
31 | val: null,
32 | children: [
33 | {
34 | type: '',
35 | val: 'hello',
36 | children: []
37 | },
38 | {
39 | type: '
',
40 | val: null,
41 | children: [
42 | {
43 | type: '
',
44 | val: null,
45 | children: [
46 | {
47 | type: '',
48 | val: 'world',
49 | children: []
50 | }
51 | ]
52 | },
53 | {
54 | type: '',
55 | val: '!',
56 | children: []
57 | }
58 | ]
59 | }
60 | ]
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/parser.js:
--------------------------------------------------------------------------------
1 | let tokens, currIndex, lookahead
2 |
3 | function nextToken () {
4 | return tokens[++currIndex]
5 | }
6 |
7 | function match (terminalType) {
8 | if (lookahead && terminalType === lookahead.type) lookahead = nextToken()
9 | else throw Error('SyntaxError')
10 | }
11 |
12 | const NT = {
13 | html () {
14 | let dom = { type: 'html', val: null, children: [] }
15 | return NT.tags(dom)
16 | },
17 | tags (currNode) {
18 | /* eslint-disable no-unmodified-loop-condition */
19 | while (lookahead) {
20 | let tagNode = { type: lookahead.val, val: null, children: [] }
21 | tagNode = NT.tag(tagNode)
22 |
23 | currNode.children.push(tagNode)
24 | if (lookahead && lookahead.type === 'TagClose') break
25 | }
26 | return currNode
27 | },
28 | tag (currNode) {
29 | match('TagOpen')
30 | if (lookahead.type === 'TagOpen') {
31 | currNode = NT.tags(currNode)
32 | } else {
33 | currNode.val = lookahead.val
34 | match('Value')
35 | }
36 | match('TagClose')
37 | return currNode
38 | }
39 | }
40 |
41 | export default {
42 | parse (t) {
43 | tokens = t
44 | currIndex = 0
45 | lookahead = tokens[currIndex]
46 | const html = NT.html()
47 | return html
48 | }
49 | }
50 |
--------------------------------------------------------------------------------