├── .gitignore ├── package.json ├── index.html ├── js ├── tokenizer.js ├── parser.js └── main.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-drawer", 3 | "version": "1.0.0", 4 | "description": "A small toy to help you draw the DOM structure", 5 | "main": "index.js", 6 | "dependencies": { 7 | "echarts": "^2.2.7-amd-beta2", 8 | "jquery": "^3.0.0" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/starkwang/DOM-Drawer.git" 17 | }, 18 | "author": "starkwang", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/starkwang/DOM-Drawer/issues" 22 | }, 23 | "homepage": "https://github.com/starkwang/DOM-Drawer#readme" 24 | } 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DOM-Drawer 6 | 46 | 47 | 48 | 49 |
50 |
51 | 52 |
53 |
54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /js/tokenizer.js: -------------------------------------------------------------------------------- 1 | module.exports = tokenizer; 2 | function tokenizer(content) { 3 | var result = []; 4 | var symbol = ['{', '}', ':', ';', ',', '(', ')', '.', '#', '~', , '<', '>', '*', '+', '[', ']', '=', '|', '^']; 5 | var isInString = false; 6 | var tmpString = ''; 7 | for (var i = 0; i < content.length; i++) { 8 | var t = content[i]; 9 | if (t == '\'' || t == '\"') { 10 | if (isInString) { 11 | tmpString += t; 12 | isInString = false; 13 | result.push(tmpString); 14 | tmpString = ''; 15 | } else { 16 | tmpString += t; 17 | isInString = true; 18 | } 19 | continue; 20 | } 21 | if (isInString) { 22 | //字符串状态 23 | tmpString += t; 24 | } else { 25 | //非字符串状态 26 | if (t == '\n' || t == ' ' || t == ' ') { 27 | if (tmpString.length != 0) { 28 | result.push(tmpString); 29 | tmpString = ''; 30 | } 31 | continue; 32 | } 33 | if (symbol.indexOf(t) != -1) { 34 | if (tmpString.length != 0) { 35 | result.push(tmpString); 36 | tmpString = ''; 37 | } 38 | result.push(t); 39 | continue; 40 | } 41 | tmpString += t; 42 | } 43 | } 44 | return result; 45 | } -------------------------------------------------------------------------------- /js/parser.js: -------------------------------------------------------------------------------- 1 | module.exports = parser; 2 | 3 | function parser(tokenArray) { 4 | var tagArray = []; 5 | var nodeHeap = []; 6 | var nodeTree = { 7 | name: 'root', 8 | children: [] 9 | }; 10 | var isInScript = false, 11 | isInStyle = false; 12 | nodeHeap.push(nodeTree); 13 | tokenArray.forEach(function(item, index) { 14 | if (item == '<') { 15 | tagArray.push(tokenArray[index + 1]); 16 | } 17 | }) 18 | console.log(tagArray); 19 | var selfEndTags = ['img', 'br', 'hr', 'col', 'area', 'link', 'meta', 'frame', 'input', 'param']; 20 | tagArray.forEach(function(item, index) { 21 | if (item[0] == '!' || selfEndTags.indexOf(item) != -1) { 22 | nodeHeap[nodeHeap.length - 1].children.push({ 23 | name: item[0] == '!' && item[1] == '-' && item[2] == '-' ? '' : item, 24 | children: [] 25 | }); 26 | } else { 27 | if (item[0] != '/') { 28 | //普通标签头 29 | if (!isInScript && !isInStyle) { 30 | var newNode = { 31 | name: item, 32 | children: [] 33 | } 34 | nodeHeap[nodeHeap.length - 1].children.push(newNode); 35 | nodeHeap.push(newNode); 36 | } 37 | if (item == 'script') { 38 | isInScript = true; 39 | } 40 | if (item == 'style') { 41 | isInStyle = true; 42 | } 43 | } else { 44 | //普通标签尾 45 | if (item.split('/')[1] == nodeHeap[nodeHeap.length - 1].name) { 46 | nodeHeap.pop(); 47 | } 48 | if (item.split('/')[1] == 'script') { 49 | isInScript = false; 50 | } 51 | if (item.split('/')[1] == 'style') { 52 | isInStyle = false; 53 | } 54 | } 55 | } 56 | }) 57 | return nodeTree; 58 | } 59 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | var echarts = require('echarts'); 2 | var $ = require('jquery'); 3 | require('echarts/config'); 4 | require('echarts/chart/tree'); 5 | var tokenizer = require('./tokenizer'); 6 | var parser = require('./parser'); 7 | function draw() { 8 | var myChart = echarts.init(document.getElementById('main')); 9 | option = { 10 | title: { 11 | text: 'DOM Drawer' 12 | }, 13 | toolbox: { 14 | show: true, 15 | feature: { 16 | restore: { 17 | show: true 18 | }, 19 | saveAsImage: { 20 | show: true 21 | } 22 | } 23 | }, 24 | series: [{ 25 | name: '树图', 26 | type: 'tree', 27 | orient: 'horizontal', // vertical horizontal 28 | rootLocation: { 29 | x: 'left', 30 | y: 'center' 31 | }, // 根节点位置 {x: 100, y: 'center'} 32 | nodePadding: 8, 33 | layerPadding: 100, 34 | hoverable: false, 35 | roam: true, 36 | symbolSize: 6, 37 | itemStyle: { 38 | normal: { 39 | color: '#4883b4', 40 | label: { 41 | show: true, 42 | position: 'right', 43 | formatter: "{b}", 44 | textStyle: { 45 | color: '#000', 46 | fontSize: 5 47 | } 48 | }, 49 | lineStyle: { 50 | color: '#ccc', 51 | type: 'curve' // 'curve'|'broken'|'solid'|'dotted'|'dashed' 52 | }, 53 | areaStyle:{ 54 | color:'#000' 55 | } 56 | }, 57 | emphasis: { 58 | color: '#4883b4', 59 | label: { 60 | show: false 61 | }, 62 | borderWidth: 0 63 | } 64 | }, 65 | 66 | data: [parser(tokenizer($('#html').val()))] 67 | }] 68 | }; 69 | myChart.setOption(option); 70 | } 71 | 72 | 73 | $('#html').keyup(function(){ 74 | draw(); 75 | }) 76 | 77 | var tmp = '\n\ 78 | \n\ 79 | \n\ 80 | DOM-Drawer\n\ 81 | \n\ 82 | \n\ 83 | \n\ 84 | \n\ 85 | \n\ 86 |

Hello,World!

\n\ 87 | \n\ 88 | '; 89 | 90 | $('#html').val(tmp); 91 | draw(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOM-Drawer 2 | A small toy to help you draw the DOM structure 3 | 4 | http://segmentfault.com/a/1190000003937571 5 | 6 | #用100行代码画出DOM树状结构 7 | 8 | 这两天写了这样一个[小玩具](http://starkwang.github.io/DOM-Drawer/),是一个可以把DOM的树状结构解析,并且画出来的东西,把HTML代码写到左边,右边就会自动生成啦。 9 | 10 | [点这里看DEMO](http://starkwang.github.io/DOM-Drawer/) 11 | 12 | 源码在[github · starkwang/DOM-Drawer](https://github.com/starkwang/DOM-Drawer),使用webpack打了个包。绘图部分依赖了百度开源的 [ECharts](http://echarts.baidu.com/),核心功能的实现只有100行代码。 13 | 14 | ------ 15 | #核心代码解读 16 | 核心代码分成两部分,tokenizer 和 parser,流程的本质上是一个最最最最简单的编译器前端。 17 | 18 | 我们期望是把类似这样的HTML字符串: 19 | 20 | ```html 21 |
22 |

23 | 24 | 25 |
26 | ``` 27 | 28 | 解析成这样的对象: 29 | 30 | ```js 31 | { 32 | name : 'div', 33 | children : [ 34 | { 35 | name : 'p', 36 | childern : [] 37 | }, 38 | { 39 | name : 'img', 40 | childern : [] 41 | }, 42 | { 43 | name : 'a', 44 | childern : [] 45 | }, 46 | ] 47 | } 48 | ``` 49 | 50 | ##Tokenizer 51 | tokenizer 负责把 HTML 字符串分割成一个由单词、特殊符号组成的数组(去掉空格、换行符、缩进),最后返回这个数组给 parser 进行解析。 52 | 53 | 54 | ```js 55 | module.exports = tokenizer; 56 | function tokenizer(content) { 57 | //结果数组 58 | var result = []; 59 | 60 | //特殊符号的集合 61 | var symbol = ['{', '}', ':', ';', ',', '(', ')', '.', '#', '~', , '<', '>', '*', '+', '[', ']', '=', '|', '^']; 62 | 63 | //是否在字符串中,如果是的话,要保留换行、缩进、空格 64 | var isInString = false; 65 | 66 | //当前的单词栈 67 | var tmpString = ''; 68 | 69 | 70 | for (var i = 0; i < content.length; i++) { 71 | //逐个读取字符 72 | var t = content[i]; 73 | 74 | //当读取到引号时,进入字符串状态 75 | if (t == '\'' || t == '\"') { 76 | if (isInString) { 77 | tmpString += t; 78 | isInString = false; 79 | result.push(tmpString); 80 | tmpString = ''; 81 | } else { 82 | tmpString += t; 83 | isInString = true; 84 | } 85 | continue; 86 | } 87 | 88 | 89 | if (isInString) { 90 | //字符串状态 91 | tmpString += t; 92 | } else { 93 | //非字符串状态 94 | 95 | if (t == '\n' || t == ' ' || t == ' ') { 96 | //如果读到了换行、空格或者tab,那么把当前单词栈中的字符作为一个单词push到结果数组中,并清零单词栈 97 | if (tmpString.length != 0) { 98 | result.push(tmpString); 99 | tmpString = ''; 100 | } 101 | continue; 102 | } 103 | if (symbol.indexOf(t) != -1) { 104 | //如果读到了特殊符号,那么把当前单词栈中的字符作为一个单词push到结果数组中,清零单词栈,再把这个特殊符号放进结果数组 105 | if (tmpString.length != 0) { 106 | result.push(tmpString); 107 | tmpString = ''; 108 | } 109 | result.push(t); 110 | continue; 111 | } 112 | //否则把字符推入单词栈中 113 | tmpString += t; 114 | } 115 | } 116 | return result; 117 | } 118 | ``` 119 | 120 | ##Parser 121 | parser负责逐个读取 tokenizer 生成的单词序列,并且解析成一个树形结构,这里用到了类似状态机的思想。 122 | 123 | ```js 124 | module.exports = parser; 125 | function parser(tokenArray) { 126 | 127 | //等下我们要从单词序列中过滤出HTML标签 128 | var tagArray = []; 129 | 130 | //节点组成的栈,用于记录状态 131 | var nodeStack = []; 132 | 133 | //根节点 134 | var nodeTree = { 135 | name: 'root', 136 | children: [] 137 | }; 138 | 139 | //是否在script、style标签内部 140 | var isInScript = false, 141 | isInStyle = false; 142 | 143 | //先把根节点推入节点栈 144 | nodeStack.push(nodeTree); 145 | 146 | //一大堆单词序列中过滤出HTML标签,注意这里没有考虑到script、style中的特殊字符 147 | tokenArray.forEach(function(item, index) { 148 | if (item == '<') { 149 | tagArray.push(tokenArray[index + 1]); 150 | } 151 | }) 152 | 153 | //HTML标准中自封闭的标签 154 | var selfEndTags = ['img', 'br', 'hr', 'col', 'area', 'link', 'meta', 'frame', 'input', 'param']; 155 | 156 | 157 | tagArray.forEach(function(item, index) { 158 | //逐个读取标签 159 | if (item[0] == '!' || selfEndTags.indexOf(item) != -1) { 160 | //自封闭标签、注释、!DOCTYPE 161 | nodeStack[nodeStack.length - 1].children.push({ 162 | name: item[0] == '!' && item[1] == '-' && item[2] == '-' ? '' : item, 163 | children: [] 164 | }); 165 | } else { 166 | //普通标签 167 | if (item[0] != '/') { 168 | //普通标签头 169 | if (!isInScript && !isInStyle) { 170 | //如果不在script或者style标签中,向节点栈尾部的children中加入这个节点,并推入这个节点,让它成为节点栈的尾部 171 | var newNode = { 172 | name: item, 173 | children: [] 174 | } 175 | nodeStack[nodeStack.length - 1].children.push(newNode); 176 | nodeStack.push(newNode); 177 | } 178 | 179 | //如果是script或者style标签,那么进入相应的状态 180 | if (item == 'script') { 181 | isInScript = true; 182 | } 183 | if (item == 'style') { 184 | isInStyle = true; 185 | } 186 | } else { 187 | //普通标签尾 188 | if (item.split('/')[1] == nodeStack[nodeStack.length - 1].name) { 189 | //如果这个标签和节点栈尾部的标签相同,那么认为这个节点终止,节点栈推出。 190 | nodeStack.pop(); 191 | } 192 | 193 | //如果是script或者style标签,那么进入相应的状态 194 | if (item.split('/')[1] == 'script') { 195 | isInScript = false; 196 | } 197 | if (item.split('/')[1] == 'style') { 198 | isInStyle = false; 199 | } 200 | } 201 | } 202 | }) 203 | return nodeTree; 204 | } 205 | ``` 206 | --------------------------------------------------------------------------------