完成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.

349
Readme.md
View File

@ -1,111 +1,157 @@
![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开发了一套简单易用的模板引擎。
> 因为我原先是个 PHPer也一直喜欢 smarty 那个模板引擎,所以在 nodeJS 上,我也喜欢能有一款类似于 smarty 的的模板引擎,可惜我所知的几个引擎中,并没有 smarty 的理念,故自己开发了一款。然而 nodeJS 并不是 php完全的模拟 smarty 又会失去 nodeJS 的味道,所以,我并不打算做 nodeJS 版的 smarty只是吸收了 smarty 的一些优秀的理念, 再结合 nodeJS开发了一套简单易用的模板引擎。
> **注:**
1. `由于时间的原因,这款模板引擎并未完成设计中所有的功能(还差extends标签和插件功能未完成)`
2. `只支持.tpl后缀的模板文件 在引用模板文件时该后缀可以省略不写。`
>
> 1. `只支持.tpl后缀的模板文件 在引用模板文件时该后缀可以省略不写。`
> 2. `模板的路径/文件名, 可以不写引号(推荐)`
## API
> 模板引擎总共就2个对外的方法简单到令人发指的地步。
### 1.assign(key, val)
- key `<String>`
- val `<String>` | `<Number>` | `<Object>` | `<Boolean>`
> 模板引擎总共就 3 个对外的方法,简单到令人发指的地步。
> 该方法用于声明一个变量,用于模板中访问和调用。
`key` 即为要声明的变量名称,须为字符串类型;
`val` 即为该变量的值,可以是常见的数据类型,不支持`Function``Class`等
### 1. config(key, val)
```javascript
* key `<String>`
* val `<Any>`
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}$/)
```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
### 2.render(tpl[, uuid])
- tpl `<String>`
- uuid `<String>` 可选
1. **cache** - 是否缓存模板编译, 默认 true
2. **path** - 模板根目录
3. **delimiter** - 模板界定符, 默认为`['<!--{', '}-->']`
### 2.assign(key, val)
* key `<String>`
* val `<纯数据类型>`
> 该方法用于声明一个变量,用于模板中访问和调用。
> `key` 即为要声明的变量名称,须为字符串类型;
> `val` 即为该变量的值,只能是纯数据类型,不支持`Function``Class`等
```javascript
smarty.assign('foo', 'bar')
smarty.assign('man', { name: 'foo', age: 18 })
smarty.assign('data', [
{ title: 'balbla', date: 'xxxx-xx' },
{ title: 'balbla blabla..', date: 'yyyy-mm' }
])
smarty.assign('readable', true)
smarty.assign('page', 20)
smarty.assign('phoneReg', /^1[34578]\d{9}$/)
```
### 3.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')
smarty.assign('foo', 'bar')
smarty
.render('index.tpl')
.then(html => {
// todo...
// eg. response.end(html)
}).catch(err => {
})
.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标签可以无限级引用。 不过一般为了可维护性, 不要太深层, 否则后期找起来,都痛苦。
### extends 标签
> **注:**
> `该标签不需要闭合`
> 用于子模板继承父模板来拓展父模板。
> **这里有几个要注意的地方**
>
> 1. `extends`标签只能放在模板的第一行, 且只能出现 1 次, 出现多个的话, 后面的都会被忽略;
> 2. 使用了`extends`标签之后, 该模板内所有的内容, 都必须使用`block`标签包起来, 否则都会被忽略;
> 3. `block`标签的顺序不作要求, 但同一个标识的`block`标签, 只能出现 1 个, 如出现多个, 则会覆盖前面的。
> 4. `extends`标签不需要闭合, 父模板的`block`标签也不需要闭合, 但子模板的`block`标签必须闭合。
这是父模板(parent.tpl)
```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
<!--
@ -124,42 +170,35 @@ include标签后接模板文件的路径(相对路径)
</body>
<!--{include 'footer'}-->
```
### each 标签
### 2. each标签
> 该标签用于在模板中遍历数组或json对象。
> 使用语法为 `each item in obj`, 或 `each i item in obj`, 只有一个参数时item即为遍历到的条目有2个参数时第1个是遍历的索引第2个为该索引对应的条目值。具体可看下面的范例。
> 该标签用于在模板中遍历数组或 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', [
smarty.assign('list', [
{ title: '标题1', date: '2017-01-01' },
{ title: '标题2', date: '2017-01-02' }
])
smarty.assign('article', {
title: '标题1',
date: '2017-01-01',
content: '这是文章内容。。。blabla'
})
smarty.assign('menu', [
{
name: '一级菜单1',
sub: [
{name: '子菜单1'},
{name: '子菜单2'},
{name: '子菜单3'},
{name: '子菜单4'}
]
sub: [{ name: '子菜单1' }, { name: '子菜单2' }]
},
{
name: '一级菜单2',
sub: [
{name: '子菜单21'},
{name: '子菜单22'},
{name: '子菜单23'},
{name: '子菜单24'}
]
sub: [{ name: '子菜单21' }, { name: '子菜单22' }]
}
])
```
```html
<body>
<!--
@ -192,7 +231,8 @@ view.assign('menu', [
<li>
<span class="idx"><!--{=++i}--></span>
<h3><!--{=item.title}--></h3>
<span><!--{=item.date}--></span></li>
<span><!--{=item.date}--></span>
</li>
<!--{/each}-->
</ul>
@ -207,16 +247,11 @@ view.assign('menu', [
</ul>
</body>
```
### if/else/elseif 标签
### 3. if/else/elseif标签
> 该标签用于在模板中进行条件判断。
> 语法为 `if condition``elseif condition`
> **注:** `该标签必须闭合`
> 该标签用于在模板中进行条件判断。语法为 `if condition``elseif condition` > **注:** `该标签必须闭合`
```html
<body>
@ -256,49 +291,39 @@ view.assign('menu', [
</body>
```
### var 标签
### 4. var标签
> 该标签用于在模板中声明一些变量,函数,用于对数据进一步的处理,理论上支持所有类型的声明定义,但不太建议在模板里定义太复杂的数据类型或方法,因为这不符合模板引擎"业务与模板分离"的理念。
> 语法为 `var key=val`
> 该标签用于在模板中声明一些变量,函数,用于对数据进一步的处理,理论上支持所有类型的声明定义,但不太建议在模板里定义太复杂的数据类型或方法,因为这不符合模板引擎"业务与模板分离"的理念。语法为 `var key=val`
```javascript
view.assign('arr', [1,3,6])
smarty.assign('arr', [1, 3, 6])
```
```html
<body>
<!--{var zhObj={1: '这是1', 3: '这是6', 6: '这是6'}}-->
<!--{var obj={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>
<p>i: <!--{=i}-->, zh: <!--{=cn(i)}--></p>
<!--{/each}-->
</body>
```
### =标签
### 5. =标签
> 该标签是最普通也是最常用的一个了,也就是用来输出一个变量的。这个标签的用法,上面也已经出现过太多了,这里就不多说什么了。
> 跟该有关的重点,请看下面的`过滤器`。
> 语法为 `=key`
> 该标签是最普通也是最常用的一个了,也就是用来输出一个变量的。这个标签的用法,上面也已经出现过太多了,这里就不多说什么了。跟该有关的重点,请看下面的`过滤器`。语法为 `=key`
> **注:**为了安全,该标签输出的文本内容,是被转义后的,转义的方式同 PHP 的 htmlspecialchars 函数
## 过滤器
> 过滤器,通俗的讲,其实也就是内置的一些方法,用来对输出的内容进行一些额外的处理。
> 语法为 `=key | filter:args`
> 过滤器名称与变量之间用 `|` 分隔,过滤器的参数用`:`分隔类似于smarty。
> 引擎内置了5个常用的过滤器后期会提供接口给开发人员自行增加.
> 过滤器,通俗的讲,其实也就是内置的一些方法,用来对输出的内容进行一些额外的处理。语法为 `=key | filter:args`
> 过滤器名称与变量之间用 `|` 分隔,过滤器的参数用`:`分隔,类似于 smarty。引擎内置了 5 个常用的过滤器,开发人员可自行增加.
### 1. html
> 该过滤器用于将被转义后的文本还原回html具体何时用看需求了。
> 该过滤器没有参数
> 该过滤器,用于将被转义后的文本,还原回 html具体何时用看需求了。该过滤器没有参数
```html
<body>
@ -314,10 +339,9 @@ view.assign('arr', [1,3,6])
</body>
```
### 2. truncate
> 该过滤器用于截取字符串。
> 该过滤器可以2个参数 截取长度(默认不截取)和拼接的字符(默认为`...`)
> 该过滤器用于截取字符串。该过滤器可以 2 个参数, 截取长度(默认不截取)和拼接的字符(默认为`...`)
```html
<body>
@ -332,8 +356,8 @@ view.assign('arr', [1,3,6])
</body>
```
### 3. lower
> 顾名思义,该过滤器用于把输出的文本,转换为小写
```html
@ -346,30 +370,29 @@ view.assign('arr', [1,3,6])
</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
> 该过滤器用于对日期的格式化,支持对字符串,时间戳,日期对象该过滤器,可以有一个参数,即定义转换的格式,语法与 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>
@ -388,3 +411,29 @@ view.assign('arr', [1,3,6])
</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

@ -5,10 +5,13 @@
*
*/
"use strict";
'use strict'
const crypto = require('crypto')
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,29 +5,34 @@
*
*/
"use strict";
'use strict'
let fs = require('fs')
let fs = require('iofs')
let path = require('path')
class Tool {
constructor(conf){
this.conf = {
constructor(opt) {
this.opt = {
delimiter: ['<!--{', '}-->'], //模板界定符
labels:{ //支持的标签类型
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])*?', //定义变量
var: 'var ([\\s\\S]*?)', //定义变量
echo: '=([^\\{\\}]*?)', //普通变量
comment: '#([\\s\\S]*?)#' //引入其他文件
}
}
this.conf = this.conf.merge(conf)
this.opt = this.opt.merge(opt)
//过滤器
this.filters = {
@ -39,8 +44,7 @@ class Tool {
str += ''
//防止模板里参数加了引号导致异常
len = len.replace(/['"]/g, '') - 0
if(str.length <= len || len < 1)
return str
if (str.length <= len || len < 1) return str
//去除参数里多余的引号
truncation = truncation.replace(/^['"]/, '').replace(/['"]$/, '')
@ -60,39 +64,52 @@ class Tool {
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(empty(key) || empty(val))
if (!key || !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])
this.opt[key] = val
}
//解析普通字段
matchNormal(m) {
let begin = this.exp('^' + this.conf.delimiter[0] + '[=\\s]?')
let end = this.exp(this.conf.delimiter[1] + '$')
let begin = this.__exp__('^' + this.opt.delimiter[0] + '[=\\s]?')
let end = this.__exp__(this.opt.delimiter[1] + '$')
m = m.replace(begin, '')
m = m
.replace(begin, '')
.replace(end, '')
.replace(/\|\|/g, "\t")
.replace(/\|\|/g, '\t')
let matches = m.split('|')
let filter = matches.length == 1 ? '' : matches[1].trim()
@ -102,7 +119,6 @@ class Tool {
txt = txt.htmlspecialchars()
if (filter) {
let args = filter.split(':')
filter = args.splice(0, 1, txt) + ''
if (filter === 'date' && args.length > 2) {
@ -114,28 +130,28 @@ class Tool {
if (this.filters.hasOwnProperty(filter)) {
args = args.map((it, i) => {
if(i === 0)
if (i === 0) {
return it
}
return `'${it}'`
})
txt = `do_fn.${filter}(${args.join(', ')})`
txt = `__filters__.${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] + '$')
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.replace(begin, '').replace(end, '')
m = m.trim()
if(empty(m) || !/\sin\s/.test(m))
if (empty(m) || !/\sin\s/.test(m)) {
return new Error('Wrong each loop')
}
let each = 'for (let '
let ms = m.split(' in ')
@ -143,7 +159,7 @@ class Tool {
let mf = ms[1].trim() //要遍历的对象
if (mi.length === 1) {
each += `d_idx in ${mf}) { let ${mi[0]} = ${mf}[d_idx]; tpl += \``;
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 += \``
}
@ -153,47 +169,39 @@ class Tool {
//解析条件语句
matchIf(m) {
let begin = this.exp('^' + this.conf.delimiter[0] + 'if\\s+')
let end = this.exp(this.conf.delimiter[1] + '$')
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.replace(begin, '').replace(end, '')
m = m.trim()
if(empty(m))
return `\`; tpl += \``
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] + '$')
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.replace(begin, '').replace(end, '')
m = m.trim()
if(empty(m))
return `\`;} else { tpl += \``
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] + '$')
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.replace(begin, '').replace(end, '')
m = m.trim()
if(!empty(m) || /=/.test(m))
m = 'let ' + m
if (!empty(m) || /=/.test(m)) m = 'let ' + m
this.vars += ` ${m};`
@ -202,79 +210,121 @@ class Tool {
//解析include
matchInclude(m) {
let begin = this.exp('^' + this.conf.delimiter[0] + 'include\\s+')
let end = this.exp(this.conf.delimiter[1] + '$')
let begin = this.__exp__('^' + this.opt.delimiter[0] + 'include\\s+')
let end = this.__exp__(this.opt.delimiter[1] + '$')
m = m.replace(begin, '')
m = m
.replace(begin, '')
.replace(end, '')
.replace(/^['"]/, '').replace(/['"]$/, '')
.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) + ''
let tpl = this.__tpl__(m)
//递归解析include
tpl = tpl.replace(/[\r\n\t]+/g, ' ') //去掉所有的换行/制表
.replace(/\\/g, '\\\\')
.replace(this.label(0), m1 => {
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$/, '') //去掉可能出现的自带的模板后缀
str += '.tpl' //统一加上后缀
str = this.__tpl__(str).replace(this.__label__('blockL'), (m, flag) => {
flag = flag.trim()
return blocks[flag] || ''
})
blocks = undefined
}
return str
}
//解析模板
parse(str, data) {
this.vars = `"use strict"; let do_fn = f; `
this.vars = `"use strict"; let __filters__ = f; `
for (let i in data) {
let tmp = JSON.stringify(data[i]) || ''
this.vars += `let ${i} = ${tmp}; `
}
str = str.replace(/[\r\n\t]+/g, ' ') //去掉所有的换行/制表
str = str
.trim()
.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.parseExtends(str)
str = this.parseNormal(str)
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

View File

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