完成smarty的开发

master
宇天 2018-05-25 15:35:26 +08:00
parent 13d6023039
commit 69e7026be8
7 changed files with 740 additions and 624 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

583
Readme.md
View File

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

80
index.js Normal file
View File

@ -0,0 +1,80 @@
/**
* nodeJS 模板引擎(依赖doJS框架)
* @authors yutent (yutent@doui.cc)
* @date 2015-12-28 13:57:12
*
*/
'use strict'
require('es.shim')
const Tool = require('./lib/tool')
const md5 = require('./lib/md5')
class Smarty {
constructor(opt) {
this.opt = { cache: true }
if (opt) {
Object.assign(this.opt, opt)
}
this.tool = new Tool(this.opt)
this.data = {} // 预定义的变量储存
this.cache = {} // 模块缓存
}
config(key, val) {
this.tool.config(key, val)
}
/**
* 定义变量
* @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 = '') {
if (!this.tool.opt.path) {
console.log(this.tool)
throw new Error('Smarty engine must define path option')
}
if (!tpl) {
return Promise.reject('argument[tpl] can not be empty')
}
if (!/\.tpl$/.test(tpl)) {
tpl += '.tpl'
}
let cacheId = md5(tpl + uuid)
if (this.opt.cache && this.cache[cacheId]) {
return Promise.resolve(this.cache[cacheId])
}
this.cache[cacheId] = this.tool.__tpl__(tpl)
try {
this.cache[cacheId] = this.tool.parse(this.cache[cacheId], this.data)
return Promise.resolve(this.cache[cacheId])
} catch (err) {
return Promise.reject(err)
}
}
}
module.exports = Smarty

View File

@ -1,89 +0,0 @@
/**
* nodeJS 模板引擎(依赖doJS框架)
* @authors yutent (yutent@doui.cc)
* @date 2015-12-28 13:57:12
*
*/
"use strict";
require('es.shim')
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

View File

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

View File

@ -5,276 +5,326 @@
* *
*/ */
"use strict"; 'use strict'
let fs = require('fs') let fs = require('iofs')
let path = require('path')
class Tool { class Tool {
constructor(opt) {
this.opt = {
delimiter: ['<!--{', '}-->'], //模板界定符
labels: {
//支持的标签类型
extends: 'extends ([^\\{\\}\\(\\)]*?)', //引入其他文件
inc: 'include ([^\\{\\}\\(\\)]*?)', //引入其他文件
each: 'each ([^\\{\\}\\(\\)]*?)', //each循环开始
done: '/each', //each循环结束
blockL: 'block ([^\\{\\}\\(\\)]*?)', //each循环开始
blockR: '/block', //each循环结束
if: 'if ([^\\{\\}\\/]*?)', //if开始
elif: 'elseif ([^\\{\\}\\/]*?)', //elseif开始
else: 'else', //else开始
fi: '/if', //if结束
var: 'var ([\\s\\S]*?)', //定义变量
echo: '=([^\\{\\}]*?)', //普通变量
comment: '#([\\s\\S]*?)#' //引入其他文件
}
}
constructor(conf){ this.opt = this.opt.merge(opt)
this.conf = {
delimiter: ['<!--{', '}-->'], //模板界定符 //过滤器
labels:{ //支持的标签类型 this.filters = {
inc: 'include([^\\{\\}\\(\\)]*?)', //引入其他文件 html: function(str = '') {
each: 'each([^\\{\\}\\(\\)]*?)', //each循环开始 str += ''
done: '/each', //each循环结束 return str.tohtml()
if: 'if([^\\{\\}\\/]*?)', //if开始 },
elif: 'elseif([^\\{\\}\\/]*?)', //elseif开始 truncate: function(str, len = '', truncation = '...') {
else: 'else', //else开始 str += ''
fi: '/if', //if结束 //防止模板里参数加了引号导致异常
var: 'var([\\s\\S])*?', //定义变量 len = len.replace(/['"]/g, '') - 0
echo: '=([^\\{\\}]*?)', //普通变量 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)
}
}
}
__tpl__(name) {
let file = path.resolve(this.opt.path, name)
if (!fs.exists(file)) {
throw new Error(`Can not find template "${file}"`)
}
return fs
.cat(file)
.toString()
.replace(/[\r\n\t]+/g, ' ') //去掉所有的换行/制表
.replace(/\\/g, '\\\\')
}
//生成正则
__exp__(str) {
return new RegExp(str, 'g')
}
//生成模板标签
__label__(id) {
let opt = this.opt
let tag = opt.labels[id]
return this.__exp__(opt.delimiter[0] + tag + opt.delimiter[1])
}
//设置 配置信息
config(key, val) {
key += ''
if (!key || !val) {
return
}
this.opt[key] = val
}
//解析普通字段
matchNormal(m) {
let begin = this.__exp__('^' + this.opt.delimiter[0] + '[=\\s]?')
let end = this.__exp__(this.opt.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 = `__filters__.${filter}(${args.join(', ')})`
}
}
return `\` + (${txt}); tpl += \``
}
//解析each循环
matchFor(m) {
let begin = this.__exp__('^' + this.opt.delimiter[0] + 'each\\s+')
let end = this.__exp__(this.opt.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.opt.delimiter[0] + 'if\\s+')
let end = this.__exp__(this.opt.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.opt.delimiter[0] + 'elseif\\s+')
let end = this.__exp__(this.opt.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.opt.delimiter[0] + 'var\\s+')
let end = this.__exp__(this.opt.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.opt.delimiter[0] + 'include\\s+')
let end = this.__exp__(this.opt.delimiter[1] + '$')
m = m
.replace(begin, '')
.replace(end, '')
.replace(/^['"]/, '')
.replace(/['"]$/, '')
.replace(/\.tpl$/, '') //去掉可能出现的自带的模板后缀
m += '.tpl' //统一加上后缀
let tpl = this.__tpl__(m)
//递归解析include
tpl = tpl.replace(this.__label__('inc'), m1 => {
return this.matchInclude(m1)
})
return tpl
}
// 解析常规标签
parseNormal(str) {
return (
str
// 解析include
.replace(this.__label__('inc'), m => {
return this.matchInclude(m)
})
// 移除注释
.replace(this.__label__('comment'), m => {
return ''
})
// 解析each循环
.replace(this.__label__('each'), m => {
return this.matchFor(m)
})
// 解析循环结束标识
.replace(this.__label__('done'), '` } tpl += `')
// 解析 if/elseif 条件
.replace(this.__label__('if'), m => {
return this.matchIf(m)
})
.replace(this.__label__('elif'), m => {
return this.matchElseIf(m)
})
// 解析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)
})
)
}
// 解析extends标签
parseExtends(str) {
let matches = str.match(/^<!--{extends ([^\\{\\}\\(\\)]*?)}-->/)
if (!matches) {
str = str
.replace(this.__label__('blockL'), '')
.replace(this.__label__('blockR'), '')
} else {
let blocks = {}
// 去除所有的extends标签, 只允许有出现1次
str = str.replace(this.__label__('extends'), '').trim()
str.replace(
/<!--{block ([^\\{\\}\\(\\)]*?)}-->([\s\S]*?)<!--{\/block}-->/g,
(m, flag, val) => {
flag = flag.trim()
blocks[flag] = val.trim()
} }
)
str = matches[1]
.replace(/^['"]/, '')
.replace(/['"]$/, '')
.replace(/\.tpl$/, '') //去掉可能出现的自带的模板后缀
this.conf = this.conf.merge(conf) str += '.tpl' //统一加上后缀
//过滤器
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)
}
}
str = this.__tpl__(str).replace(this.__label__('blockL'), (m, flag) => {
flag = flag.trim()
return blocks[flag] || ''
})
blocks = undefined
} }
return str
}
//设置 配置信息 //解析模板
config(key, val){ parse(str, data) {
key += '' this.vars = `"use strict"; let __filters__ = f; `
if(empty(key) || empty(val)) for (let i in data) {
return let tmp = JSON.stringify(data[i]) || ''
this.conf[key] = val this.vars += `let ${i} = ${tmp}; `
} }
str = str
.trim()
.replace(/[\r\n\t]+/g, ' ') // 去掉所有的换行/制表
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
//生成正则 str = this.parseExtends(str)
exp(str){ str = this.parseNormal(str)
return new RegExp(str, 'g')
}
//生成模板标签 str = `${this.vars} let tpl=\`${str}\`; return tpl;`
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)
}
return new Function('f', str)(this.filters)
}
} }
module.exports = Tool
module.exports = Tool

View File

@ -1,9 +1,9 @@
{ {
"name": "smartyx", "name": "smartyx",
"version": "0.0.3", "version": "1.0.0",
"description": "nodeJS模板引擎理念源自于PHP的smarty模板引擎", "description": "nodeJS模板引擎理念源自于PHP的smarty模板引擎",
"keywords": [ "keywords": [
"dojs", "fivejs",
"smarty", "smarty",
"template", "template",
"ejs", "ejs",
@ -12,11 +12,13 @@
"author": "宇天 <yutent@doui.cc>", "author": "宇天 <yutent@doui.cc>",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.oschina.net/yutent/smartyx.git" "url": "https://github.com/yutent/smarty.git"
}, },
"dependencies": { "dependencies": {
"es.shim": "^0.0.2" "es.shim": "^1.0.0",
"iofs": "^1.1.0"
}, },
"devDependencies": {}, "devDependencies": {},
"main": "lib/main.js" "main": "lib/main.js",
"license": "MIT"
} }