From 69e7026be89ca3bbf1a01db99d4910061a6cd068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=87=E5=A4=A9?= Date: Fri, 25 May 2018 15:35:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90smarty=E7=9A=84=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 ++ Readme.md | 583 ++++++++++++++++++++++++++++----------------------- index.js | 80 +++++++ lib/main.js | 89 -------- lib/md5.js | 13 +- lib/tool.js | 566 ++++++++++++++++++++++++++----------------------- package.json | 12 +- 7 files changed, 740 insertions(+), 624 deletions(-) create mode 100644 LICENSE create mode 100644 index.js delete mode 100644 lib/main.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab60297 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Readme.md b/Readme.md index 546b769..33dc918 100644 --- a/Readme.md +++ b/Readme.md @@ -1,114 +1,160 @@ ![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 -> 模板引擎总共就2个对外的方法,简单到令人发指的地步。 -### 1.assign(key, val) -- key `` -- val `` | `` | `` | `` +> 模板引擎总共就 3 个对外的方法,简单到令人发指的地步。 + +### 1. config(key, val) + +* key `` +* val `` + +> 该方法用于设置一些额外的参数, 如模板的根目录,是否缓存等 + +```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 `` +* val `<纯数据类型>` > 该方法用于声明一个变量,用于模板中访问和调用。 -`key` 即为要声明的变量名称,须为字符串类型; -`val` 即为该变量的值,可以是常见的数据类型,不支持`Function`,`Class`等 +> `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}$/) - +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]) -### 2.render(tpl[, uuid]) -- tpl `` -- uuid `` 可选 +* tpl `` +* uuid `` 可选 -> 该方法用于渲染一个模板,返回值为一个 Promise对象; +> 该方法用于渲染一个模板,返回值为一个 Promise 对象; > `tpl` 即为要渲染的模板的绝对路径,默认是`.tpl`后缀, 该后缀可以省略。 -> `uuid` 是一个唯一标识,用于开启模板缓存,但又想页面渲染的时候,可以根据不同的情况渲染不同的内容。 - -**注:** 该功能目前并未进行优化。 - +> `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... - }) - +smarty.assign('foo', 'bar') +smarty + .render('index.tpl') + .then(html => { + // todo... + // eg. response.end(html) + }) + .catch(err => { + // debug... + }) ``` - -## 引擎的配置 -> 引擎在实例化的时候,支持作一些配置,目前只支持2个配置项: -- cache `` -该值,顾名思义,就是设置模板的缓存,默认是开启缓存的,意味着,在模板本身没有发生改变,或服务发生重启之前,引擎不会重新渲染,而都是从缓存中读取。 -- delimiter `` -该值是用来设置模板的界定符,值为一个数组,默认值`['']`,切勿设置为太常规的,如`['<', '>']`, `['{', '}']`,否则会解析出错。 - -```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": ""}, "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": ""}, "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 - - give me five + + + + + + + + + + + +``` + +这是这是子模板 + +```html + + + + + + + + + + + +

Hello Smarty X

+ +``` + +### block 标签 + +> block 标签必须搭配 extends 标签使用, 单独使用会被移除。子模板的 block 标签的标识不能重复, 但是父模板的 block 标识,可以重复。 + +### `#` 标签 + +> 也就是注释标签,``, 该注释标签里的内容,在编译的时候都会被移除。支持多行注释 + +### include 标签 + +> 该标签用于在模板中加载另外的模板文件,一般多用于,将公共模板单独拆分引用,以便于 修改一处,即可实现所有用到该公共模板的页面同时修改。被引入的模板中,同样可以使用 include 标签,可以无限级引用。 不过一般为了可维护性, 不要太深层。 + +> **注:** > `该标签不需要闭合` + +```html + @@ -116,275 +162,278 @@ include标签,后接模板文件的路径(相对路径), - -
- -
- + +
+ +
+ - ``` +### 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', [ - { - name: '一级菜单1', - sub: [ - {name: '子菜单1'}, - {name: '子菜单2'}, - {name: '子菜单3'}, - {name: '子菜单4'} - ] - }, - { - name: '一级菜单2', - sub: [ - {name: '子菜单21'}, - {name: '子菜单22'}, - {name: '子菜单23'}, - {name: '子菜单24'} - ] - } +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: '一级菜单2', + sub: [{ name: '子菜单21' }, { name: '子菜单22' }] + } ]) ``` - ```html - - - - -
    - -
  • :
  • - -
+ + +
    + +
  • + +

    + +
  • + +
+ + + +
    + +
  • :
  • + +
- ``` +### if/else/elseif 标签 - -### 3. if/else/elseif标签 -> 该标签用于在模板中进行条件判断。 -> 语法为 `if condition` 或 `elseif condition` -> **注:** `该标签必须闭合` - +> 该标签用于在模板中进行条件判断。语法为 `if condition` 或 `elseif condition` > **注:** `该标签必须闭合` ```html - -
    - -
  • class="red" > - -

    - -
  • - -
+ +
    + +
  • class="red" > + +

    + +
  • + +
- -
    - -
  • - -

    - -
  • - -
+ +
    + +
  • + +

    + +
  • + +
- -
    - -
  • - -

    - -
  • - -
+ +
    + +
  • + +

    + +
  • + +
``` +### var 标签 - -### 4. var标签 -> 该标签用于在模板中声明一些变量,函数,用于对数据进一步的处理,理论上支持所有类型的声明定义,但不太建议在模板里定义太复杂的数据类型或方法,因为这不符合模板引擎"业务与模板分离"的理念。 -> 语法为 `var key=val` +> 该标签用于在模板中声明一些变量,函数,用于对数据进一步的处理,理论上支持所有类型的声明定义,但不太建议在模板里定义太复杂的数据类型或方法,因为这不符合模板引擎"业务与模板分离"的理念。语法为 `var key=val` ```javascript -view.assign('arr', [1,3,6]) - +smarty.assign('arr', [1, 3, 6]) ``` ```html - - + + - -

i: , zh:

- + +

i: , zh:

+ ``` +### =标签 - -### 5. =标签 -> 该标签是最普通也是最常用的一个了,也就是用来输出一个变量的。这个标签的用法,上面也已经出现过太多了,这里就不多说什么了。 -> 跟该有关的重点,请看下面的`过滤器`。 -> 语法为 `=key` -> **注:**为了安全,该标签输出的文本内容,是被转义后的,转义的方式同PHP的htmlspecialchars函数 - - - +> 该标签是最普通也是最常用的一个了,也就是用来输出一个变量的。这个标签的用法,上面也已经出现过太多了,这里就不多说什么了。跟该有关的重点,请看下面的`过滤器`。语法为 `=key` +> **注:**为了安全,该标签输出的文本内容,是被转义后的,转义的方式同 PHP 的 htmlspecialchars 函数 ## 过滤器 -> 过滤器,通俗的讲,其实也就是内置的一些方法,用来对输出的内容进行一些额外的处理。 -> 语法为 `=key | filter:args` -> 过滤器名称与变量之间用 `|` 分隔,过滤器的参数用`:`分隔,类似于smarty。 -> 引擎内置了5个常用的过滤器,后期会提供接口给开发人员自行增加. + +> 过滤器,通俗的讲,其实也就是内置的一些方法,用来对输出的内容进行一些额外的处理。语法为 `=key | filter:args` +> 过滤器名称与变量之间用 `|` 分隔,过滤器的参数用`:`分隔,类似于 smarty。引擎内置了 5 个常用的过滤器,开发人员可自行增加. ### 1. html -> 该过滤器,用于将被转义后的文本,还原回html,具体何时用,看需求了。 -> 该过滤器没有参数 + +> 该过滤器,用于将被转义后的文本,还原回 html,具体何时用,看需求了。该过滤器没有参数 ```html - - - - - + + - - + + + + + ``` - ### 2. truncate -> 该过滤器用于截取字符串。 -> 该过滤器可以2个参数, 截取长度(默认不截取)和拼接的字符(默认为`...`) + +> 该过滤器用于截取字符串。该过滤器可以 2 个参数, 截取长度(默认不截取)和拼接的字符(默认为`...`) ```html - - - - + - - + + + + + ``` - ### 3. lower + > 顾名思义,该过滤器用于把输出的文本,转换为小写 ```html - - - - + + + + ``` - ### 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 - - - - - + + + + + - - + + - - + + ``` + +--- + +## 额外福利 + +> 因为模板引擎默认使用``界定符, 为了方便快速插入,这里提供了一份 sublime 的快捷键配置,可以快速插入该模板标签: + +``` +{ "keys": ["ctrl+shift+["], + "command": "insert_snippet", + "args": {"contents": ""}, + "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": ""}, + "context": [ + { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true }, + { "key": "selection_empty", "operator": "equal", "operand": false, "match_all": true } + ] +} +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..e0dc4ca --- /dev/null +++ b/index.js @@ -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 diff --git a/lib/main.js b/lib/main.js deleted file mode 100644 index c0eeb91..0000000 --- a/lib/main.js +++ /dev/null @@ -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 - diff --git a/lib/md5.js b/lib/md5.js index 3fdb163..ba4e4d9 100644 --- a/lib/md5.js +++ b/lib/md5.js @@ -1,14 +1,17 @@ /** - * + * * @authors yutent (yutent@doui.cc) * @date 2017-01-17 15:50:51 * */ -"use strict"; +'use strict' const crypto = require('crypto') -module.exports = function(str = ''){ - return crypto.createHash('md5').update(str + '', 'utf8').digest('hex') -} \ No newline at end of file +module.exports = function(str = '') { + return crypto + .createHash('md5') + .update(str + '', 'utf8') + .digest('hex') +} diff --git a/lib/tool.js b/lib/tool.js index f843dbe..9ad03c3 100644 --- a/lib/tool.js +++ b/lib/tool.js @@ -5,276 +5,326 @@ * */ -"use strict"; +'use strict' -let fs = require('fs') +let fs = require('iofs') +let path = require('path') 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.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.opt = this.opt.merge(opt) + + //过滤器 + 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) + } + } + } + + __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(/^/) + 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( + /([\s\S]*?)/g, + (m, flag, val) => { + flag = flag.trim() + blocks[flag] = val.trim() } + ) + str = matches[1] + .replace(/^['"]/, '') + .replace(/['"]$/, '') + .replace(/\.tpl$/, '') //去掉可能出现的自带的模板后缀 - 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) - } - - } + str += '.tpl' //统一加上后缀 + str = this.__tpl__(str).replace(this.__label__('blockL'), (m, flag) => { + flag = flag.trim() + return blocks[flag] || '' + }) + blocks = undefined } + return str + } - //设置 配置信息 - config(key, val){ - key += '' - if(empty(key) || empty(val)) - return - this.conf[key] = val + //解析模板 + parse(str, data) { + 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 + .trim() + .replace(/[\r\n\t]+/g, ' ') // 去掉所有的换行/制表 + .replace(/\\/g, '\\\\') + .replace(/`/g, '\\`') - //生成正则 - exp(str){ - return new RegExp(str, 'g') - } + str = this.parseExtends(str) + str = this.parseNormal(str) - //生成模板标签 - 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) - } + str = `${this.vars} let tpl=\`${str}\`; return tpl;` + return new Function('f', str)(this.filters) + } } - -module.exports = Tool \ No newline at end of file +module.exports = Tool diff --git a/package.json b/package.json index 97d8089..7c547b8 100644 --- a/package.json +++ b/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": "宇天 ", "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" }