init project

master
宇天 2017-03-17 18:07:35 +08:00
parent fba09df449
commit 536814d7d9
5 changed files with 762 additions and 12 deletions

393
Readme.md
View File

@ -1,11 +1,388 @@
# 模板引擎
# smartyx > 因为我原先是个PHPer也一直喜欢smarty那个模板引擎所以在nodeJS上我也喜欢能有一款类似于smarty的的模板引擎可惜我所知的几个引擎中并没有smarty的理念故自己开发了一款。
>Node.js的模板引擎理念源于PHP的smarty但不是smarty的Node.js实现。 然而nodeJS并不是php完全的模拟smarty又会失去nodeJS的味道所以我并不打算做nodeJS版的smarty只是吸收了smarty的一些优秀的理念 再结合nodeJS开发了一套简单易用的模板引擎。
>因为这不是PHPNode.js有自己的特点所以未不打算照搬smarty更不会按php的语法。仅仅是吸收smarty中优秀的理念。 > **注:**
1. `由于时间的原因,这款模板引擎并未完成设计中所有的功能(还差extends标签和插件功能未完成)`
2. `只支持.tpl后缀的模板文件 在引用模板文件时该后缀可以省略不写。`
**注**
目前该0.0.1版本只是为了占位正式版将于近日发布版本号并从1.0.0开始命名。
## API
> 模板引擎总共就2个对外的方法简单到令人发指的地步。
### 1.assign(key, val)
- key `<String>`
- val `<String>` | `<Number>` | `<Object>` | `<Boolean>`
> 该方法用于声明一个变量,用于模板中访问和调用。
`key` 即为要声明的变量名称,须为字符串类型;
`val` 即为该变量的值,可以是常见的数据类型,不支持`Function``Class`等
```javascript
let view = new (require('dojs-template'))()
view.assign('foo', 'bar')
view.assign('man', {name: 'foo', age: 18})
view.assign('data', [{title: 'balbla', date: 'xxxx-xx'}, {title: 'balbla blabla..', date: 'yyyy-mm'}])
view.assign('readable', true)
view.assign('page', 20)
view.assign('phoneReg', /^1[34578]\d{9}$/)
```
### 2.render(tpl[, uuid])
- tpl `<String>`
- uuid `<String>` 可选
> 该方法用于渲染一个模板,返回值为一个 Promise对象;
> `tpl` 即为要渲染的模板的绝对路径,默认是`.tpl`后缀, 该后缀可以省略。
> `uuid` 是一个唯一标识,用于开启模板缓存,但又想页面渲染的时候,可以根据不同的情况渲染不同的内容。
**注:** 该功能目前并未进行优化。
```javascript
let view = new (require('dojs-template'))()
view.assign('foo', 'bar')
view.render('/views/index.tpl')
.then(html => {
// todo...
// eg. response.end(html)
}).catch(err => {
// debug...
})
```
## 引擎的配置
> 引擎在实例化的时候支持作一些配置目前只支持2个配置项
- cache `<Boolean>`
该值,顾名思义,就是设置模板的缓存,默认是开启缓存的,意味着,在模板本身没有发生改变,或服务发生重启之前,引擎不会重新渲染,而都是从缓存中读取。
- delimiter `<Array>`
该值是用来设置模板的界定符,值为一个数组,默认值`['<!--{', '}-->']`,切勿设置为太常规的,如`['<', '>']`, `['{', '}']`,否则会解析出错。
```javascript
//关闭缓存功能
let view = new (require('dojs-template'))({cache: false})
//设置界定符为 '{{', '}}',一般情况下,不建议修改这个
let view = new (require('dojs-template'))({delimiter: ['{{', '}}']})
```
这里提供了一份sublime的快捷键配置可以快速插入该模板标签
```javascript
{ "keys": ["ctrl+shift+["], "command": "insert_snippet", "args": {"contents": "<!--{${0}}-->"}, "context":
[
{ "key": "setting.auto_match_enabled", "operator": "equal", "operand": true },
{ "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true },
{ "key": "following_text", "operator": "regex_contains", "operand": "^(?:\t||\\)|]|\\}|>|$)", "match_all": true }
]
},
{ "keys": ["ctrl+shift+["], "command": "insert_snippet", "args": {"contents": "<!--{${0:$SELECTION}}-->"}, "context":
[
{ "key": "setting.auto_match_enabled", "operator": "equal", "operand": true },
{ "key": "selection_empty", "operator": "equal", "operand": false, "match_all": true }
]
}
```
---
## 模板标签示例
### 1. include标签
> 该标签用于在模板中加载另外的模板文件,一般多用于,将公共模板单独拆分引用,以便于 修改一处,即可实现所有用到该公共模板的页面同时修改。
被引入的模板中同样可以使用include标签可以无限级引用。 不过一般为了可维护性, 不要太深层, 否则后期找起来,都痛苦。
> **注:**
> `该标签不需要闭合`
```html
<!--
include标签后接模板文件的路径(相对路径)
模板名称可以不用引号括起来(推荐不写),模板文件后缀也可以不写,如下面的例子
-->
<!--{include header}-->
<body>
<!--{include 'nav.tpl'}-->
<div class="main wrap">
<!-- your code here -->
</div>
<!--{include friends.tpl}-->
</body>
<!--{include 'footer'}-->
```
### 2. each标签
> 该标签用于在模板中遍历数组或json对象。
> 使用语法为 `each item in obj`, 或 `each i item in obj`, 只有一个参数时item即为遍历到的条目有2个参数时第1个是遍历的索引第2个为该索引对应的条目值。具体可看下面的范例。
> **注:** `该标签必须闭合`
```javascript
view.assign('list', [{title: '标题1', date: '2017-01-01'}, {title: '标题2', date: '2017-01-02'}])
view.assign('article', {title: '标题1', date: '2017-01-01', content: '这是文章内容。。。blabla'})
view.assign('menu', [
{
name: '一级菜单1',
sub: [
{name: '子菜单1'},
{name: '子菜单2'},
{name: '子菜单3'},
{name: '子菜单4'}
]
},
{
name: '一级菜单2',
sub: [
{name: '子菜单21'},
{name: '子菜单22'},
{name: '子菜单23'},
{name: '子菜单24'}
]
}
])
```
```html
<body>
<!--
each标签支持多维数组 但要注意变量不要重复使用,以免出现非预想的结果
-->
<div class="menu">
<!--{each it in menu}-->
<ul>
<li class="name"><!--{=it.name}--></li>
<li class="sub-name-box">
<ul>
<!--{each sub in it.sub}-->
<li class="sub-name"><!--{=sub.name}--></li>
<!--{/each}-->
</ul>
</li>
</ul>
<!--{/each}-->
</div>
<!--
纯数组的遍历i对应的即是 索引值了从0开始,
但是一般输出到页面上时都是从1开始排这时候有2种方式
1. 使用普通的运算表达式, i-0+1, 这里的先减0是为了把字符串 i 转为数字类型(因为模板引擎解析模板的时候,把数字类型转成了字符串类型,所以这里要作个小处理)
2. 使用自增的写法,即 ++i; 这种方法简洁一点但是会改变i本身的值所以后面要用到i的时候要注意一下此时的值。
-->
<ul class="list">
<!--{each i item in article}-->
<li>
<span class="idx"><!--{=++i}--></span>
<h3><!--{=item.title}--></h3>
<span><!--{=item.date}--></span></li>
<!--{/each}-->
</ul>
<!--
each遍历json对象时2个参数对应的便是 key和value
-->
<ul class="article">
<!--{each k v in article}-->
<li><!--{=k}-->: <!--{=v}--></li>
<!--{/each}-->
</ul>
</body>
```
### 3. if/else/elseif标签
> 该标签用于在模板中进行条件判断。
> 语法为 `if condition``elseif condition`
> **注:** `该标签必须闭合`
```html
<body>
<!-- 依然以上面的为例, 偶数行 添加类 red -->
<ul class="list">
<!--{each i item in article}-->
<li <!--{if i%2 === 0}-->class="red" <!--{/if}-->>
<span class="idx"><!--{=++i}--></span>
<h3><!--{=item.title}--></h3>
<span><!--{=item.date}--></span>
</li>
<!--{/each}-->
</ul>
<!-- 偶数行加类red否则加green -->
<ul class="list">
<!--{each i item in article}-->
<li class="<!--{if i%2 === 0}--> red <!--{else}--> green <!--{/if}-->">
<span class="idx"><!--{=++i}--></span>
<h3><!--{=item.title}--></h3>
<span><!--{=item.date}--></span>
</li>
<!--{/each}-->
</ul>
<!-- 首行加bold, red 剩下的 偶数行加类red否则加green -->
<ul class="list">
<!--{each i item in article}-->
<li class="<!--{if i == 0}--> bold red <!--{elseif i%2 === 0}--> red <!--{else}--> green <!--{/if}-->">
<span class="idx"><!--{=++i}--></span>
<h3><!--{=item.title}--></h3>
<span><!--{=item.date}--></span>
</li>
<!--{/each}-->
</ul>
</body>
```
### 4. var标签
> 该标签用于在模板中声明一些变量,函数,用于对数据进一步的处理,理论上支持所有类型的声明定义,但不太建议在模板里定义太复杂的数据类型或方法,因为这不符合模板引擎"业务与模板分离"的理念。
> 语法为 `var key=val`
```javascript
view.assign('arr', [1,3,6])
```
```html
<body>
<!--{var zhObj={1: '这是1', 3: '这是6', 6: '这是6'}}-->
<!--{var cn=function(v){return obj[v]}}-->
<!--{each i in arr}-->
<p>i: <!--{=tt(i)}-->, zh: <!--{=cn(i)}--></p>
<!--{/each}-->
</body>
```
### 5. =标签
> 该标签是最普通也是最常用的一个了,也就是用来输出一个变量的。这个标签的用法,上面也已经出现过太多了,这里就不多说什么了。
> 跟该有关的重点,请看下面的`过滤器`。
> 语法为 `=key`
> **注:**为了安全该标签输出的文本内容是被转义后的转义的方式同PHP的htmlspecialchars函数
## 过滤器
> 过滤器,通俗的讲,其实也就是内置的一些方法,用来对输出的内容进行一些额外的处理。
> 语法为 `=key | filter:args`
> 过滤器名称与变量之间用 `|` 分隔,过滤器的参数用`:`分隔类似于smarty。
> 引擎内置了5个常用的过滤器后期会提供接口给开发人员自行增加.
### 1. html
> 该过滤器用于将被转义后的文本还原回html具体何时用看需求了。
> 该过滤器没有参数
```html
<body>
<!--{var txt1='<span>这段文本没有使用过滤器</span>'}-->
<!--{var txt2='<span>这段文本使用了html过滤器</span>'}-->
<!-- 这里输出的结果是 &lt;span&gt;这段文本没有使用过滤器&lt;/span&gt; -->
<!--{=txt1}-->
<!-- 这里输出的结果将是一个正常的span节点-->
<!--{=txt2 | html}-->
</body>
```
### 2. truncate
> 该过滤器用于截取字符串。
> 该过滤器可以2个参数 截取长度(默认不截取)和拼接的字符(默认为`...`)
```html
<body>
<!--{var txt='这一段很长很长很长的文本这一段很长很长很长的文本这一段很长很长很长的文本这一段很长很长很长的文本'}-->
<!-- 这里输出的结果是 '这一段很长...' -->
<!--{=txt | truncate:5}-->
<!-- 这里输出的结果是 '这一段很长很长~~~' -->
<!--{=txt | truncate:7:~~~}-->
</body>
```
### 3. lower
> 顾名思义,该过滤器用于把输出的文本,转换为小写
```html
<body>
<!--{var txt='HELLO WORLD'}-->
<!-- 这里输出的结果是 'hello world' -->
<!--{=txt | lower}-->
</body>
```
### 4. upper
> 相应的,该过滤器用于将输出的文本转换为大写的
### 5. date
> 该过滤器用于对日期的格式化,支持对字符串,时间戳,日期对象
> 该过滤器可以有一个参数即定义转换的格式语法与php的date函数一致(默认为 Y-m-d H:i:s)
> - Y 4位数年份
> - y 短格式的年份(不建议用了)
> - m 2位数份01~12
> - n 月份(不会自动补0)1-12
> - d 2位数日期 01-31
> - j 日期(不会自动补0)1-31
> - H 小时(24小时制自动补0) 00-23
> - h 小时(12小时制自动补0) 00-12
> - G 小时(24小时制, 不会自动补0) 0-23
> - g 小时(12小时制, 不会自动补0) 0-12
> - i 分钟(自动补0), 00-59
> - s 秒钟(自动补0), 00-59
> - W 当前是本年度第几周
> - w 当前是本月第几周
> - D 星期,英文缩写 Mon, Tues, Wed, Thur, Fri, Sat, Sun
```html
<body>
<!--{var txt1='2017-01-12 23:33:33'}-->
<!--{var txt2=1485167755953}-->
<!-- 这里输出的结果是 2017/01/12 -->
<!--{=txt1 | date:Y/m/d}-->
<!-- 这里输出的结果是 2017-01-23 18:35:55 -->
<!--{=txt2 | date}-->
<!-- 这里输出的结果是 2017年01月23日 18点35分55秒 -->
<!--{=txt2 | date:Y年m月d日 H点i分s秒}-->
</body>
```

View File

@ -1,10 +1,89 @@
/** /**
* * nodeJS 模板引擎(依赖doJS框架)
* @authors yutent (yutent@doui.cc) * @authors yutent (yutent@doui.cc)
* @date 2017-02-05 14:44:11 * @date 2015-12-28 13:57:12
* *
*/ */
"use strict"; "use strict";
require('dojs-extend')
const Tool = require('./tool'),
fs = require('fs'),
path = require('path'),
md5 = require('./md5');
class Smarty {
constructor(conf){
this.conf = {}
if(!Object.empty(conf))
this.conf = conf
this.conf.cache = this.conf.hasOwnProperty('cache') ? this.conf.cache : true
this.tool = new Tool(conf)
this.data = {} //预定义的变量储存
this.cache = {} //模块缓存
}
/**
* 定义变量
* @param {Str} key 变量名
* @param {any} val
*/
assign(key, val){
key += ''
if(!key)
return this
this.data[key] = val
return this
}
/**
* [render 模板渲染]
* @param {String} tpl 模板路径
* @param {String} uuid 唯一标识
* @return {Promise} 返回一个Promise对象
*/
render(tpl = '', uuid = ''){
return new Promise((yes, no) => {
if(!tpl)
return no('argument[tpl] can not be empty')
if(!/\.tpl$/.test(tpl))
tpl += '.tpl'
let cacheId = md5(tpl + uuid);
if(this.conf.cache && this.cache[cacheId])
return yes(this.cache[cacheId])
if(!fs.existsSync(tpl))
return no('Can not find template "' + tpl + '"')
this.tool.config('path', path.parse(tpl).dir + '/')
this.cache[cacheId] = fs.readFileSync(tpl) + ''
try{
this.cache[cacheId] = this.tool.parse(this.cache[cacheId], this.data)
yes(this.cache[cacheId])
}catch(err){
no(err)
}
})
}
}
module.exports = Smarty

14
lib/md5.js Normal file
View File

@ -0,0 +1,14 @@
/**
*
* @authors yutent (yutent@doui.cc)
* @date 2017-01-17 15:50:51
*
*/
"use strict";
const crypto = require('crypto')
module.exports = function(str = ''){
return crypto.createHash('md5').update(str + '', 'utf8').digest('hex')
}

280
lib/tool.js Normal file
View File

@ -0,0 +1,280 @@
/**
* 模板引擎预处理对dojs框架有依赖
* @authors yutent (yutent@doui.cc)
* @date 2016-01-02 21:26:49
*
*/
"use strict";
let fs = require('fs')
class Tool {
constructor(conf){
this.conf = {
delimiter: ['<!--{', '}-->'], //模板界定符
labels:{ //支持的标签类型
inc: 'include([^\\{\\}\\(\\)]*?)', //引入其他文件
each: 'each([^\\{\\}\\(\\)]*?)', //each循环开始
done: '/each', //each循环结束
if: 'if([^\\{\\}\\/]*?)', //if开始
elif: 'elseif([^\\{\\}\\/]*?)', //elseif开始
else: 'else', //else开始
fi: '/if', //if结束
var: 'var([\\s\\S])*?', //定义变量
echo: '=([^\\{\\}]*?)', //普通变量
}
}
this.conf = this.conf.merge(conf)
//过滤器
this.filters = {
html: function(str = ''){
str += ''
return str.tohtml()
},
truncate: function(str, len = '', truncation = '...'){
str += ''
//防止模板里参数加了引号导致异常
len = len.replace(/['"]/g, '') - 0
if(str.length <= len || len < 1)
return str
//去除参数里多余的引号
truncation = truncation.replace(/^['"]/, '').replace(/['"]$/, '')
return str.slice(0, len) + truncation
},
lower: function(str){
str += ''
return str.toLowerCase()
},
upper: function(str){
str += ''
return str.toUpperCase()
},
date: function(str, format = ''){
//去除参数里多余的引号
format = format.replace(/^['"]/, '').replace(/['"]$/, '')
return gmdate(format, str)
}
}
}
//设置 配置信息
config(key, val){
key += ''
if(empty(key) || empty(val))
return
this.conf[key] = val
}
//生成正则
exp(str){
return new RegExp(str, 'g')
}
//生成模板标签
label(id){
let conf = this.conf
let tag = conf.labels[id || 'inc']
return this.exp(conf.delimiter[0] + tag + conf.delimiter[1])
}
//解析普通字段
matchNormal(m){
let begin = this.exp('^' + this.conf.delimiter[0] + '[=\\s]?')
let end = this.exp(this.conf.delimiter[1] + '$')
m = m.replace(begin, '')
.replace(end, '')
.replace(/\|\|/g, "\t")
let matches = m.split('|')
let filter = matches.length == 1 ? '' : matches[1].trim()
let txt = matches[0].replace(/\t/g, '||').trim()
// 默认过滤HTML标签
txt = txt.htmlspecialchars()
if(filter){
let args = filter.split(':')
filter = args.splice(0, 1, txt) + ''
if(filter === 'date' && args.length > 2){
let tmp = args.splice(0, 1)
tmp.push(args.join(':'))
args = tmp
tmp = null
}
if(this.filters.hasOwnProperty(filter)){
args = args.map((it, i) => {
if(i === 0)
return it
return `'${it}'`
})
txt = `do_fn.${filter}(${args.join(', ')})`
}
}
return `\` + (${txt}); tpl += \``
}
//解析each循环
matchFor(m){
let begin = this.exp('^' + this.conf.delimiter[0] + 'each\\s+')
let end = this.exp(this.conf.delimiter[1] + '$')
m = m.replace(begin, '')
.replace(end, '')
m = m.trim()
if(empty(m) || !/\sin\s/.test(m))
return new Error('Wrong each loop')
let each = 'for (let '
let ms = m.split(' in ')
let mi = ms[0].trim().split(' ')
let mf = ms[1].trim() //要遍历的对象
if(mi.length === 1){
each += `d_idx in ${mf}) { let ${mi[0]} = ${mf}[d_idx]; tpl += \``;
}else{
each += `${mi[0]} in ${mf}) { let ${mi[1]} = ${mf}[${mi[0]}]; tpl += \``
}
return `\`; ${each}`
}
//解析条件语句
matchIf(m){
let begin = this.exp('^' + this.conf.delimiter[0] + 'if\\s+')
let end = this.exp(this.conf.delimiter[1] + '$')
m = m.replace(begin, '')
.replace(end, '')
m = m.trim()
if(empty(m))
return `\`; tpl += \``
return `\`; if (${m}){ tpl += \``
}
//解析条件语句
matchElseIf(m){
let begin = this.exp('^' + this.conf.delimiter[0] + 'elseif\\s+')
let end = this.exp(this.conf.delimiter[1] + '$')
m = m.replace(begin, '')
.replace(end, '')
m = m.trim()
if(empty(m))
return `\`;} else { tpl += \``
return `\`; } else if (${m}){ tpl += \``
}
//解析变量定义
matchVar(m){
let begin = this.exp('^' + this.conf.delimiter[0] + 'var\\s+')
let end = this.exp(this.conf.delimiter[1] + '$')
m = m.replace(begin, '')
.replace(end, '')
m = m.trim()
if(!empty(m) || /=/.test(m))
m = 'let ' + m
this.vars += ` ${m};`
return `\`; tpl += \``
}
//解析include
matchInclude(m){
let begin = this.exp('^' + this.conf.delimiter[0] + 'include\\s+')
let end = this.exp(this.conf.delimiter[1] + '$')
m = m.replace(begin, '')
.replace(end, '')
.replace(/^['"]/, '').replace(/['"]$/, '')
.replace(/\.tpl$/, '') //去掉可能出现的自带的模板后缀
m += '.tpl' //统一加上后缀
if(!fs.existsSync(this.conf.path + m))
return new Error('Can not find template "' + m + '"')
let tpl = fs.readFileSync(this.conf.path + m) + ''
//递归解析include
tpl = tpl.replace(/[\r\n\t]+/g, ' ') //去掉所有的换行/制表
.replace(/\\/g, '\\\\')
.replace(this.label(0), m1 => {
return this.matchInclude(m1)
})
return tpl
}
//解析模板
parse(str, data){
this.vars = `"use strict"; let do_fn = f; `
for(let i in data){
let tmp = JSON.stringify(data[i]) || ''
this.vars += `let ${i} = ${tmp}; `
}
str = str.replace(/[\r\n\t]+/g, ' ') //去掉所有的换行/制表
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
//解析include
.replace(this.label('inc'), m => {
return this.matchInclude(m)
})
//解析each循环
.replace(this.label('each'), m => {
return this.matchFor(m)
})
//解析循环结束标识
.replace(this.label('done'), '\` } tpl += \`')
//解析 if条件
.replace(this.label('if'), m => {
return this.matchIf(m)
})
.replace(this.label('elif'), m => {
return this.matchElseIf(m)
})
// parse the else
.replace(this.label('else'), '\`; } else { tpl += \`')
//解析if条件结束标识
.replace(this.label('fi'), '\`; } tpl += \`')
//解析临时变量的定义
.replace(this.label('var'), m => {
return this.matchVar(m)
})
//解析普通变量/字段
.replace(this.label('echo'), m => {
return this.matchNormal(m)
})
str = `${this.vars} let tpl=\`${str}\`; return tpl;`
return (new Function('f', str))(this.filters)
}
}
module.exports = Tool

View File

@ -1,6 +1,6 @@
{ {
"name": "smartyx", "name": "smartyx",
"version": "0.0.1", "version": "0.0.2",
"description": "nodeJS模板引擎理念源自于PHP的smarty模板引擎", "description": "nodeJS模板引擎理念源自于PHP的smarty模板引擎",
"keywords": [ "keywords": [
"dojs", "dojs",