2017-08-01
模版引擎
template
模版解释器的实现
解释器最重要的事情就是取得 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

这一步将进行语法树的解释求值。需要注意
  1. 是递归的遍历
  2. 求值还需要动态创建和释放作用域
  3. 变量查找是在作用域链上进行的
  4. 如果命中的变量是函数,按照 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

# 2018 Update

实现重构为 TypeScript




回到顶部