解释器最重要的事情就是取得 AST , 求值的过程就是遍历 AST 的过程。
模版引擎本质上就是解释执行一门 “模版语言” ,并得到输出,它针对的是字符串处理那方面的工作,算是一种 DSL 。
这篇文章主要是讨论
[email protected]
的实现 (一个模版引擎)https://github.com/eczn/tplser
# template -> statements ↵
这一步的目的是抽取出模版中
有语义的块
,这种块其实就是 ejs 里面用 <% %>
包住的部分。为了写起来爽那么一些,tplser 采用
{{ }}
作为语义钦点符。而从模版里面抽取出语句,我采用正则匹配的方式去做:
00var EXP = /{{(.*?)}}/g; 01var statements = []; 02var template = ` 03 <h1>{{ hello }}</h1> 04 <p>{{ varEczn }}</p> 05`; 06 07template.replace(EXP, (match, p1, offset) => { 08 statements.push({ 09 token: match, 10 offset: offset 11 }); 12}); 13 14console.log(statements);
得到的
statements
如下 :00[ 01 { 02 "token": "{{ hello }}", 03 "offset": 9 04 }, 05 { 06 "token": "{{ varEczn }}", 07 "offset": 33 08 } 09]
其中
offset
是该块在 template
中的首地址, token
是匹配到的语义块下一步即是继续处理上面这个
statements
# (statements, template) -> codeTokens ↵
得到 statements 后,下一步是处理无语义的部分,使其变成模版语法中最为基本的元素存在。
做到这些要完成函数:
codeTokenGenerator(statements, template);
不多说 上代码:
00// codeTokenGenerator.js 01module.exports = (statements, template) => { 02 // Reduce For Statements 03 return statements.reduce((acc, stat, idx, its) => { 04 if (idx === 0){ 05 // 处理第一个 statement 06 acc.push({ 07 isCode: false, 08 token: template.slice(0, stat.offset) 09 }) 10 } else { 11 // 其他情况 12 acc.push({ 13 isCode: false, 14 token: template.slice( 15 its[idx - 1].offset + its[idx - 1].token.length, 16 stat.offset 17 ) 18 }) 19 } 20 21 // push 22 acc.push({ 23 isCode: true, 24 token: stat.token 25 }); 26 27 return acc; 28 }, []).concat([ 29 // 上面那个 reduce 会漏掉最后一个 在这里补上 30 { 31 isCode: false, 32 token: template.slice( 33 statements[statements.length - 1].offset + statements[statements.length - 1].token.length 34 ) 35 } 36 ]); 37}
搭配先前的 statements 来使用的结果是:
00var codeTokens = codeTokenGenerator(statements, template);
模版换成下面这个
00 {{ get item >>>> list }} 01 <h1>{{ item }}</h1> 02 {{ teg }}
00var codeTokens = [ 01 { 02 "isCode": false, 03 "token": "\n\t" 04 }, 05 { 06 "isCode": true, 07 "token": "{{ get item >>>> list }}" 08 }, 09 { 10 "isCode": false, 11 "token": "\n\t\t<h1>" 12 }, 13 { 14 "isCode": true, 15 "token": "{{ item }}" 16 }, 17 { 18 "isCode": false, 19 "token": "</h1>\n\t" 20 }, 21 { 22 "isCode": true, 23 "token": "{{ teg }}" 24 }, 25 { 26 "isCode": false, 27 "token": "\n" 28 } 29]
这一步把 statements 转化成如上格式。有趣的是,这样可以将 codeTokens 完全还原成模版:
00var template = codeTokens.reduce((acc, cur) => acc + cur.token, ''); 01// => 02// {{ get item >>>> list }} 03// <h1>{{ item }}</h1> 04// {{ teg }}
当然还没完,token 还要再处理一下。 这一段比较蛋疼,而且基本上是从 Q-lang 上面借鉴过来的。 因此折叠了。
折叠的代码
00// tokenParser 01 02var parseToArr = str => str.split('\t').join(' ').split('').reduce((acc, cur) => { 03 if (cur === ','){ 04 // 把逗号当成空格处理 05 acc.push(' '); 06 return acc; 07 } 08 if (cur === '(' || cur === '['){ 09 acc.push(' '); 10 acc.push(cur); 11 acc.push(' '); 12 return acc; 13 } else if (cur === ')' || cur === ']') { 14 acc.push(' '); 15 acc.push(cur); 16 acc.push(' '); 17 } else { 18 acc.push(cur); 19 } 20 21 return acc; 22}, []).join('').split(' ').map(ch => { 23 if (!Number.isNaN(parseInt(ch))){ 24 return parseInt(ch); 25 } else { 26 return ch; 27 } 28}).filter(e => e !== '' && e!== '\n'); 29 30 31module.exports = token => { 32 var halfTokens = parseToArr(token); 33 34 var temp; 35 36 37 if (halfTokens[0] === 'get'){ 38 let item, idx; 39 40 if (halfTokens.length <= 3){ 41 item = '$item' 42 idx = '$index'; 43 } else { 44 if (halfTokens[1] === '('){ 45 item = halfTokens[2]; 46 if (halfTokens[3] === ')'){ 47 idx = '$index'; 48 } else { 49 idx = halfTokens[3]; 50 } 51 } else { 52 item = halfTokens[1]; 53 idx = '$index'; 54 } 55 } 56 57 temp = { 58 todo: 'get', 59 key: halfTokens[0], 60 // itme 或者 index 可能缺省 61 item: item, 62 index: idx, 63 // 最后一个是数组 64 list: halfTokens[halfTokens.length-1] 65 } 66 67 console.log(halfTokens) 68 console.log(temp); 69 } else if (halfTokens[0] === 'teg') { 70 temp = { 71 todo: 'teg' 72 } 73 } else if (halfTokens[0] === 'if'){ 74 temp = { 75 todo: 'if', 76 key: halfTokens.slice(1) 77 } 78 } else if (halfTokens[0] === 'else'){ 79 temp = { 80 todo: 'else' 81 } 82 } else if (halfTokens[0] === 'fi'){ 83 temp = { 84 todo: 'fi' 85 } 86 } else { 87 temp = { 88 todo: 'render', 89 key: halfTokens 90 } 91 } 92 93 return temp; 94}
最后一步处理之后 codeTokens 应该是这样的:
00[ 01 { 02 "isCode":false, 03 "token":"\n\t" 04 }, 05 { 06 "isCode": true, 07 "token":{ 08 "todo":"get", 09 "key":"get", 10 "item":"item", 11 "index":"$index", 12 "list":"list" 13 } 14 }, 15 { 16 "isCode": false, 17 "token":"\n\t\t<h1>" 18 }, 19 { 20 "isCode": true, 21 "token":{ 22 "todo":"render", 23 "key":["item"] 24 } 25 }, 26 { 27 "isCode": false, 28 "token":"</h1>\n\t" 29 }, 30 { 31 "isCode": true, 32 "token":{ 33 "todo":"teg" 34 } 35 }, 36 { 37 "isCode": false, 38 "token":"\n" 39 } 40]
# codeTokens -> syntaxs ↵
从这一步开始 开始真正的
解释器
的开发之旅。这一步的目的是把
codeTokens
转化成语法树。转化成语法树,调用以下函数即可。
00// syntaxParser.js 01var syntaxParser = codeTokens => { 02 var res = [], deep = 0; 03 04 for (let i = 0; i < codeTokens.length; i++){ 05 let codeToken = codeTokens[i] 06 , { isCode, token } = codeToken; 07 08 if (isCode){ 09 if (token.todo === 'get' || token.todo === 'if'){ 10 deep = deep + 1; 11 if (deep === 1){ 12 let temp = syntaxParser( 13 codeTokens.slice(i + 1) 14 ); 15 temp.o = token; 16 res.push(temp); 17 } 18 } else if (token.todo === 'teg' || token.todo === 'fi') { 19 deep = deep - 1; 20 if (deep < 0) return res; 21 } else { 22 if (deep === 0){ 23 res.push(codeToken); 24 } 25 } 26 } else { 27 if (deep === 0){ 28 res.push(codeToken); 29 } 30 } 31 } 32 33 return res; 34} 35 36module.exports = syntaxParser;
读懂上面的代码需要注意以下事项:
- get 语句是可以嵌套的(循环可以嵌套),因此是递归的解析的
- 读取到 get 的时候会递归自己
- 遇到闭合的 get 会收束递归 返回 res
# syntaxs -> string ↵
这一步将进行语法树的解释求值。需要注意
- 是递归的遍历
- 求值还需要动态创建和释放作用域
- 变量查找是在作用域链上进行的
- 如果命中的变量是函数,按照 S 表达式的方式进行解析
000// sytaxer.js 001 002// 作用域链寻找变量 003function find(keyStr, scopes){ 004 var keys = keyStr.split('.') 005 , key = keys[0] 006 , fir = null; 007 008 // For Performance 009 for (let i = 0; i < scopes.length; i ++){ 010 let now = scopes[i]; 011 if (key in now){ 012 fir = now[key]; 013 break; 014 } 015 } 016 017 return keys.slice(1).reduce((acc, key) => { 018 return acc[key]; 019 }, fir); 020} 021 022// get 求值 023var getEval = (syntaxs, scopes) => { 024 // 创建作用域 025 var newScope = {}, list = find(syntaxs.o.list, scopes) 026 027 // 压入作用域链 028 scopes.unshift(newScope); 029 030 var res = ''; 031 for (let idx = 0; idx < list.length; idx ++) { 032 newScope[syntaxs.o.index] = idx; 033 newScope[syntaxs.o.item] = list[idx]; 034 035 res += sytaxer(syntaxs, scopes) 036 } 037 038 // 弹出作用域链 039 scopes.shift(); 040 041 return res; 042} 043 044// if 求值 045var ifEval = (syntaxs, scopes) => { 046 var how2if = syntaxs.o; 047 var ifCondition = renderEval(how2if.key, scopes); 048 049 // 取得 else 从句的位置 没有 else 则为 -1 050 var whereElse = syntaxs.reduce((acc, cur, idx) => { 051 if (cur.token.todo === 'else'){ 052 return idx; 053 } else { 054 return acc; 055 } 056 }, -1); 057 058 var temp; 059 060 if (ifCondition) { 061 if (~whereElse) { 062 // Has Else 063 temp = syntaxs.slice(0, whereElse); 064 } else { 065 // No Else 066 temp = syntaxs; 067 } 068 } else { 069 // 不包括 else 自己 070 temp = syntaxs.slice(whereElse + 1); 071 } 072 073 return sytaxer(temp, scopes); 074} 075 076// 值渲染 函数处理 077var renderEval = (key, scopes) => { 078 var findInScopes = key => find(key, scopes); 079 var opt = find(key[0], scopes); 080 if (typeof opt === 'function'){ 081 return opt.apply( 082 // 顶部作用域 083 scopes[0], 084 // 参数表 085 key.slice(1).map(findInScopes) 086 ); 087 } else { 088 return opt; 089 } 090} 091 092 093function sytaxer(syntaxs, scopes){ 094 // 求值 095 return syntaxs.reduce((acc, syntax) => { 096 if (Array.isArray(syntax)){ 097 098 if (syntax.o.todo === 'get'){ 099 // get 语句 100 return acc + getEval(syntax, scopes); 101 } else { 102 // if 语句 103 return acc + ifEval(syntax, scopes); 104 } 105 } else { 106 // 叶子 107 if (syntax.isCode) { 108 // render 109 if (syntax.token.todo === 'render') { 110 111 return acc + renderEval(syntax.token.key, scopes); 112 } 113 } else { 114 // Just Plain Text 115 return acc + syntax.token; 116 } 117 } 118 }, ''); 119} 120 121module.exports = sytaxer;
至此一个模版解释器就写好了。
# TEST ↵
具体的测试啊,可以自己 npm install 看看:
https://github.com/eczn/tplser
https://www.npmjs.com/package/tplser
https://www.npmjs.com/package/tplser
# 2018 Update ↵
实现重构为 TypeScript