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