完成smarty的开发
parent
13d6023039
commit
69e7026be8
|
@ -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
349
Readme.md
|
@ -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 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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
|
89
lib/main.js
89
lib/main.js
|
@ -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
|
||||
|
|
@ -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')
|
||||
}
|
260
lib/tool.js
260
lib/tool.js
|
@ -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
|
12
package.json
12
package.json
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue