diff --git a/Readme.md b/Readme.md index 43a1c61..b035c19 100644 --- a/Readme.md +++ b/Readme.md @@ -1,11 +1,388 @@ - -# smartyx ->Node.js的模板引擎,理念源于PHP的smarty,但不是smarty的Node.js实现。 ->因为这不是PHP,Node.js有自己的特点,所以未不打算照搬smarty,更不会按php的语法。仅仅是吸收smarty中优秀的理念。 - - -**注** -目前该0.0.1版本只是为了占位,正式版将于近日发布,版本号并从1.0.0开始命名。 +# 模板引擎 +> 因为我原先是个PHPer,也一直喜欢smarty那个模板引擎,所以在nodeJS上,我也喜欢能有一款类似于smarty的的模板引擎,可惜我所知的几个引擎中,并没有smarty的理念,故自己开发了一款。 +然而nodeJS并不是php,完全的模拟smarty又会失去nodeJS的味道,所以,我并不打算做nodeJS版的smarty,只是吸收了smarty的一些优秀的理念, 再结合nodeJS,开发了一套简单易用的模板引擎。 +> **注:** +1. `由于时间的原因,这款模板引擎并未完成设计中所有的功能(还差extends标签和插件功能未完成)` +2. `只支持.tpl后缀的模板文件, 在引用模板文件时该后缀可以省略不写。` +## API +> 模板引擎总共就2个对外的方法,简单到令人发指的地步。 + +### 1.assign(key, val) +- key `` +- val `` | `` | `` | `` + +> 该方法用于声明一个变量,用于模板中访问和调用。 +`key` 即为要声明的变量名称,须为字符串类型; +`val` 即为该变量的值,可以是常见的数据类型,不支持`Function`,`Class`等 + +```javascript + +let view = new (require('dojs-template'))() + +view.assign('foo', 'bar') +view.assign('man', {name: 'foo', age: 18}) +view.assign('data', [{title: 'balbla', date: 'xxxx-xx'}, {title: 'balbla blabla..', date: 'yyyy-mm'}]) +view.assign('readable', true) +view.assign('page', 20) +view.assign('phoneReg', /^1[34578]\d{9}$/) + +``` + + +### 2.render(tpl[, uuid]) +- tpl `` +- uuid `` 可选 + +> 该方法用于渲染一个模板,返回值为一个 Promise对象; +> `tpl` 即为要渲染的模板的绝对路径,默认是`.tpl`后缀, 该后缀可以省略。 +> `uuid` 是一个唯一标识,用于开启模板缓存,但又想页面渲染的时候,可以根据不同的情况渲染不同的内容。 + +**注:** 该功能目前并未进行优化。 + + +```javascript + +let view = new (require('dojs-template'))() + +view.assign('foo', 'bar') +view.render('/views/index.tpl') + .then(html => { + // todo... + // eg. response.end(html) + }).catch(err => { + // debug... + }) + +``` + + +## 引擎的配置 +> 引擎在实例化的时候,支持作一些配置,目前只支持2个配置项: +- cache `` +该值,顾名思义,就是设置模板的缓存,默认是开启缓存的,意味着,在模板本身没有发生改变,或服务发生重启之前,引擎不会重新渲染,而都是从缓存中读取。 +- 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标签,可以无限级引用。 不过一般为了可维护性, 不要太深层, 否则后期找起来,都痛苦。 + +> **注:** +> `该标签不需要闭合` + +```html + + + + + + +
+ +
+ + + + + +``` + + + +### 2. each标签 +> 该标签用于在模板中遍历数组或json对象。 +> 使用语法为 `each item in obj`, 或 `each i item in obj`, 只有一个参数时,item即为遍历到的条目,有2个参数时,第1个是遍历的索引,第2个为该索引对应的条目值。具体可看下面的范例。 +> **注:** `该标签必须闭合` + +```javascript +view.assign('list', [{title: '标题1', date: '2017-01-01'}, {title: '标题2', date: '2017-01-02'}]) +view.assign('article', {title: '标题1', date: '2017-01-01', content: '这是文章内容。。。blabla'}) +view.assign('menu', [ + { + name: '一级菜单1', + sub: [ + {name: '子菜单1'}, + {name: '子菜单2'}, + {name: '子菜单3'}, + {name: '子菜单4'} + ] + }, + { + name: '一级菜单2', + sub: [ + {name: '子菜单21'}, + {name: '子菜单22'}, + {name: '子菜单23'}, + {name: '子菜单24'} + ] + } +]) +``` + + +```html + + + + + + +
    + +
  • + +

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

    + +
  • + +
+ + +
    + +
  • + +

    + +
  • + +
+ + +
    + +
  • + +

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

i: , zh:

+ + + +``` + + + +### 5. =标签 +> 该标签是最普通也是最常用的一个了,也就是用来输出一个变量的。这个标签的用法,上面也已经出现过太多了,这里就不多说什么了。 +> 跟该有关的重点,请看下面的`过滤器`。 +> 语法为 `=key` +> **注:**为了安全,该标签输出的文本内容,是被转义后的,转义的方式同PHP的htmlspecialchars函数 + + + + +## 过滤器 +> 过滤器,通俗的讲,其实也就是内置的一些方法,用来对输出的内容进行一些额外的处理。 +> 语法为 `=key | filter:args` +> 过滤器名称与变量之间用 `|` 分隔,过滤器的参数用`:`分隔,类似于smarty。 +> 引擎内置了5个常用的过滤器,后期会提供接口给开发人员自行增加. + +### 1. html +> 该过滤器,用于将被转义后的文本,还原回html,具体何时用,看需求了。 +> 该过滤器没有参数 + +```html + + + + + + + + + + + +``` + + +### 2. truncate +> 该过滤器用于截取字符串。 +> 该过滤器可以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 + +```html + + + + + + + + + + + + + + + +``` diff --git a/lib/main.js b/lib/main.js index c28f92d..a9efc22 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,10 +1,89 @@ /** - * + * nodeJS 模板引擎(依赖doJS框架) * @authors yutent (yutent@doui.cc) - * @date 2017-02-05 14:44:11 + * @date 2015-12-28 13:57:12 * */ - "use strict"; +require('dojs-extend') +const Tool = require('./tool'), + fs = require('fs'), + path = require('path'), + md5 = require('./md5'); + +class Smarty { + + constructor(conf){ + this.conf = {} + if(!Object.empty(conf)) + this.conf = conf + + this.conf.cache = this.conf.hasOwnProperty('cache') ? this.conf.cache : true + + this.tool = new Tool(conf) + this.data = {} //预定义的变量储存 + this.cache = {} //模块缓存 + } + + /** + * 定义变量 + * @param {Str} key 变量名 + * @param {any} val 值 + */ + assign(key, val){ + key += '' + if(!key) + return this + + this.data[key] = val + return this + } + + + /** + * [render 模板渲染] + * @param {String} tpl 模板路径 + * @param {String} uuid 唯一标识 + * @return {Promise} 返回一个Promise对象 + */ + render(tpl = '', uuid = ''){ + + return new Promise((yes, no) => { + + if(!tpl) + return no('argument[tpl] can not be empty') + + if(!/\.tpl$/.test(tpl)) + tpl += '.tpl' + + let cacheId = md5(tpl + uuid); + + if(this.conf.cache && this.cache[cacheId]) + return yes(this.cache[cacheId]) + + if(!fs.existsSync(tpl)) + return no('Can not find template "' + tpl + '"') + + this.tool.config('path', path.parse(tpl).dir + '/') + this.cache[cacheId] = fs.readFileSync(tpl) + '' + + try{ + this.cache[cacheId] = this.tool.parse(this.cache[cacheId], this.data) + yes(this.cache[cacheId]) + }catch(err){ + no(err) + } + + }) + + } + +} + + + + + +module.exports = Smarty diff --git a/lib/md5.js b/lib/md5.js new file mode 100644 index 0000000..3fdb163 --- /dev/null +++ b/lib/md5.js @@ -0,0 +1,14 @@ +/** + * + * @authors yutent (yutent@doui.cc) + * @date 2017-01-17 15:50:51 + * + */ + +"use strict"; + +const crypto = require('crypto') + +module.exports = function(str = ''){ + return crypto.createHash('md5').update(str + '', 'utf8').digest('hex') +} \ No newline at end of file diff --git a/lib/tool.js b/lib/tool.js new file mode 100644 index 0000000..f843dbe --- /dev/null +++ b/lib/tool.js @@ -0,0 +1,280 @@ +/** + * 模板引擎预处理,对dojs框架有依赖 + * @authors yutent (yutent@doui.cc) + * @date 2016-01-02 21:26:49 + * + */ + +"use strict"; + +let fs = require('fs') + +class Tool { + + constructor(conf){ + this.conf = { + delimiter: [''], //模板界定符 + labels:{ //支持的标签类型 + inc: 'include([^\\{\\}\\(\\)]*?)', //引入其他文件 + each: 'each([^\\{\\}\\(\\)]*?)', //each循环开始 + done: '/each', //each循环结束 + if: 'if([^\\{\\}\\/]*?)', //if开始 + elif: 'elseif([^\\{\\}\\/]*?)', //elseif开始 + else: 'else', //else开始 + fi: '/if', //if结束 + var: 'var([\\s\\S])*?', //定义变量 + echo: '=([^\\{\\}]*?)', //普通变量 + } + } + + this.conf = this.conf.merge(conf) + + //过滤器 + this.filters = { + html: function(str = ''){ + str += '' + return str.tohtml() + }, + truncate: function(str, len = '', truncation = '...'){ + str += '' + //防止模板里参数加了引号导致异常 + len = len.replace(/['"]/g, '') - 0 + if(str.length <= len || len < 1) + return str + + //去除参数里多余的引号 + truncation = truncation.replace(/^['"]/, '').replace(/['"]$/, '') + + return str.slice(0, len) + truncation + }, + lower: function(str){ + str += '' + return str.toLowerCase() + }, + upper: function(str){ + str += '' + return str.toUpperCase() + }, + date: function(str, format = ''){ + //去除参数里多余的引号 + format = format.replace(/^['"]/, '').replace(/['"]$/, '') + return gmdate(format, str) + } + + } + + } + + //设置 配置信息 + config(key, val){ + key += '' + if(empty(key) || empty(val)) + return + this.conf[key] = val + } + + //生成正则 + exp(str){ + return new RegExp(str, 'g') + } + + //生成模板标签 + label(id){ + let conf = this.conf + let tag = conf.labels[id || 'inc'] + return this.exp(conf.delimiter[0] + tag + conf.delimiter[1]) + } + + //解析普通字段 + matchNormal(m){ + let begin = this.exp('^' + this.conf.delimiter[0] + '[=\\s]?') + let end = this.exp(this.conf.delimiter[1] + '$') + + m = m.replace(begin, '') + .replace(end, '') + .replace(/\|\|/g, "\t") + + let matches = m.split('|') + let filter = matches.length == 1 ? '' : matches[1].trim() + let txt = matches[0].replace(/\t/g, '||').trim() + + // 默认过滤HTML标签 + txt = txt.htmlspecialchars() + + if(filter){ + + let args = filter.split(':') + filter = args.splice(0, 1, txt) + '' + if(filter === 'date' && args.length > 2){ + let tmp = args.splice(0, 1) + tmp.push(args.join(':')) + args = tmp + tmp = null + } + + if(this.filters.hasOwnProperty(filter)){ + args = args.map((it, i) => { + if(i === 0) + return it + return `'${it}'` + }) + txt = `do_fn.${filter}(${args.join(', ')})` + } + + } + return `\` + (${txt}); tpl += \`` + } + + //解析each循环 + matchFor(m){ + let begin = this.exp('^' + this.conf.delimiter[0] + 'each\\s+') + let end = this.exp(this.conf.delimiter[1] + '$') + + m = m.replace(begin, '') + .replace(end, '') + + m = m.trim() + if(empty(m) || !/\sin\s/.test(m)) + return new Error('Wrong each loop') + + let each = 'for (let ' + let ms = m.split(' in ') + let mi = ms[0].trim().split(' ') + let mf = ms[1].trim() //要遍历的对象 + + if(mi.length === 1){ + each += `d_idx in ${mf}) { let ${mi[0]} = ${mf}[d_idx]; tpl += \``; + }else{ + each += `${mi[0]} in ${mf}) { let ${mi[1]} = ${mf}[${mi[0]}]; tpl += \`` + } + + return `\`; ${each}` + } + + //解析条件语句 + matchIf(m){ + let begin = this.exp('^' + this.conf.delimiter[0] + 'if\\s+') + let end = this.exp(this.conf.delimiter[1] + '$') + + m = m.replace(begin, '') + .replace(end, '') + + m = m.trim() + if(empty(m)) + return `\`; tpl += \`` + + return `\`; if (${m}){ tpl += \`` + } + + //解析条件语句 + matchElseIf(m){ + let begin = this.exp('^' + this.conf.delimiter[0] + 'elseif\\s+') + let end = this.exp(this.conf.delimiter[1] + '$') + + m = m.replace(begin, '') + .replace(end, '') + + m = m.trim() + if(empty(m)) + return `\`;} else { tpl += \`` + + return `\`; } else if (${m}){ tpl += \`` + } + + //解析变量定义 + matchVar(m){ + let begin = this.exp('^' + this.conf.delimiter[0] + 'var\\s+') + let end = this.exp(this.conf.delimiter[1] + '$') + + + + m = m.replace(begin, '') + .replace(end, '') + + m = m.trim() + if(!empty(m) || /=/.test(m)) + m = 'let ' + m + + this.vars += ` ${m};` + + return `\`; tpl += \`` + } + + //解析include + matchInclude(m){ + let begin = this.exp('^' + this.conf.delimiter[0] + 'include\\s+') + let end = this.exp(this.conf.delimiter[1] + '$') + + m = m.replace(begin, '') + .replace(end, '') + .replace(/^['"]/, '').replace(/['"]$/, '') + .replace(/\.tpl$/, '') //去掉可能出现的自带的模板后缀 + + m += '.tpl' //统一加上后缀 + + if(!fs.existsSync(this.conf.path + m)) + return new Error('Can not find template "' + m + '"') + + let tpl = fs.readFileSync(this.conf.path + m) + '' + //递归解析include + tpl = tpl.replace(/[\r\n\t]+/g, ' ') //去掉所有的换行/制表 + .replace(/\\/g, '\\\\') + .replace(this.label(0), m1 => { + return this.matchInclude(m1) + }) + + return tpl + } + + + //解析模板 + parse(str, data){ + + this.vars = `"use strict"; let do_fn = f; ` + for(let i in data){ + let tmp = JSON.stringify(data[i]) || '' + this.vars += `let ${i} = ${tmp}; ` + } + + str = str.replace(/[\r\n\t]+/g, ' ') //去掉所有的换行/制表 + .replace(/\\/g, '\\\\') + .replace(/`/g, '\\`') + //解析include + .replace(this.label('inc'), m => { + return this.matchInclude(m) + }) + //解析each循环 + .replace(this.label('each'), m => { + return this.matchFor(m) + }) + //解析循环结束标识 + .replace(this.label('done'), '\` } tpl += \`') + //解析 if条件 + .replace(this.label('if'), m => { + return this.matchIf(m) + }) + .replace(this.label('elif'), m => { + return this.matchElseIf(m) + }) + // parse the else + .replace(this.label('else'), '\`; } else { tpl += \`') + //解析if条件结束标识 + .replace(this.label('fi'), '\`; } tpl += \`') + //解析临时变量的定义 + .replace(this.label('var'), m => { + return this.matchVar(m) + }) + //解析普通变量/字段 + .replace(this.label('echo'), m => { + return this.matchNormal(m) + }) + + str = `${this.vars} let tpl=\`${str}\`; return tpl;` + + return (new Function('f', str))(this.filters) + } + +} + + +module.exports = Tool \ No newline at end of file diff --git a/package.json b/package.json index 235abbb..66c7c1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smartyx", - "version": "0.0.1", + "version": "0.0.2", "description": "nodeJS模板引擎,理念源自于PHP的smarty模板引擎", "keywords": [ "dojs",