├── .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 | --------------------------------------------------------------------------------