diff --git a/index.js b/index.js index e3fc850..d103c82 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,8 @@ /** * nodeJS 模板引擎 - * @authors yutent (yutent@doui.cc) - * @date 2015-12-28 13:57:12 - * + * @author yutent + * @date 2020/09/18 13:36:47 */ -'use strict' require('es.shim') diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..74fa0d3 --- /dev/null +++ b/index.mjs @@ -0,0 +1,100 @@ +/** + * nodeJS 模板引擎 + * @author yutent + * @date 2020/09/18 13:36:47 + */ + +import 'es.shim' +import path from 'path' +import fs from 'iofs' + +import Tool from './lib/tool.mjs' + +function hash(str) { + return Buffer.from(str).toString('hex') +} + +export default class Smarty { + constructor(opt) { + this.opt = { cache: true, ext: '.htm' } + if (opt) { + Object.assign(this.opt, opt) + } + + this.__REG__ = new RegExp(this.opt.ext + '$') + this.tool = new Tool(this.opt) + this.__DATA__ = Object.create(null) // 预定义的变量储存 + this.__CACHE__ = Object.create(null) // 渲染缓存 + } + + config(key, val) { + key += '' + if (!key || val === undefined) { + return + } + this.opt[key] = val + this.tool.opt[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} filePath 模板路径 + * @param {Boolean} noParse 不解析直接读取 + * @return {Promise} 返回一个Promise对象 + */ + render(filePath = '', noParse = false) { + var key = null + var cache + if (!this.opt.path) { + throw new Error('Smarty engine must define path option') + } + if (!filePath) { + return Promise.reject('argument[filePath] can not be empty') + } + + if (!this.__REG__.test(filePath)) { + filePath += this.opt.ext + } + filePath = path.resolve(this.opt.path, filePath) + + key = hash(filePath) + + if (this.__CACHE__[key]) { + return Promise.resolve(fs.cat(path.resolve('./cache/', key))) + } + + cache = this.tool.__readFile__(filePath, noParse) + + if (noParse) { + this.__CACHE__[key] = true + fs.echo(cache, path.resolve('./cache/', key)) + return Promise.resolve(cache) + } + + try { + cache = this.tool.parse(cache, this.__DATA__) + if (this.opt.cache) { + this.__CACHE__[key] = true + fs.echo(cache, path.resolve('./cache/', key)) + } + return Promise.resolve(cache) + } catch (err) { + return Promise.reject(err) + } + } +} diff --git a/lib/tool.js b/lib/tool.js index 0397c7a..bfd8012 100644 --- a/lib/tool.js +++ b/lib/tool.js @@ -1,12 +1,9 @@ /** * 模板引擎预处理 - * @authors yutent (yutent@doui.cc) - * @date 2016-01-02 21:26:49 - * + * @author yutent + * @date 2020/09/18 13:46:19 */ -'use strict' - const fs = require('iofs') const path = require('path') diff --git a/lib/tool.mjs b/lib/tool.mjs new file mode 100644 index 0000000..18ff568 --- /dev/null +++ b/lib/tool.mjs @@ -0,0 +1,334 @@ +/** + * 模板引擎预处理 + * @author yutent + * @date 2020/09/18 13:46:19 + */ + +import path from 'path' +import fs from 'iofs' + +export default 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]*?)#' //引入其他文件 + } + } + + Object.assign(this.opt, opt) + + this.__REG__ = new RegExp(this.opt.ext + '$') + + //过滤器 + 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(/['"]$/, '') + if (isFinite(str)) { + str = +str + } + return new Date(str).format(format) + } + } + } + + __readFile__(file, noParse) { + var buf = null + if (!fs.exists(file)) { + throw new Error(`Can not find template "${file}"`) + } + + buf = fs.cat(file).toString() + + if (noParse) { + return buf + } + + return buf + .replace(/[\r\n\t]+/g, ' ') //去掉所有的换行/制表 + .replace(/\\/g, '\\\\') + } + + //生成正则 + __exp__(str) { + return new RegExp(str, 'g') + } + + //生成模板标签 + __label__(id) { + var opt = this.opt + var tag = opt.labels[id] + return this.__exp__(opt.delimiter[0] + tag + opt.delimiter[1]) + } + + //解析普通字段 + 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 (!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 (!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 (!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 (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] + '$') + var tpl = '' + + m = m + .replace(begin, '') + .replace(end, '') + .replace(/^['"]/, '') + .replace(/['"]$/, '') + .replace(this.__REG__, '') //去掉可能出现的自带的模板后缀 + + m += this.opt.ext //统一加上后缀 + + tpl = this.__readFile__(path.resolve(this.opt.path, 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(this.__REG__, '') //去掉可能出现的自带的模板后缀 + + str += this.opt.ext //统一加上后缀 + + str = this.__readFile__(path.resolve(this.opt.path, str)).replace( + this.__label__('blockL'), + (m, flag) => { + flag = flag.trim() + return blocks[flag] || '' + } + ) + } + return str + } + + //解析模板 + parse(str, data) { + var vars = `"use strict"; let __filters__ = f; ` + for (let i in data) { + let tmp = JSON.stringify(data[i]) || '' + vars += `let ${i} = ${tmp}; ` + } + str = str + .trim() + .replace(/[\r\n\t]+/g, ' ') // 去掉所有的换行/制表 + .replace(/\\/g, '\\\\') + .replace(/`/g, '\\`') + + str = this.parseExtends(str) + str = this.parseNormal(str) + + str = `${vars} let tpl=\`${str}\`; return tpl;` + + return new Function('f', str)(this.filters) + } +} diff --git a/node_modules/es.shim b/node_modules/es.shim new file mode 120000 index 0000000..92a23a5 --- /dev/null +++ b/node_modules/es.shim @@ -0,0 +1 @@ +../../../bytedo/es.shim \ No newline at end of file diff --git a/node_modules/iofs b/node_modules/iofs new file mode 120000 index 0000000..1a63549 --- /dev/null +++ b/node_modules/iofs @@ -0,0 +1 @@ +../../../bytedo/iofs \ No newline at end of file diff --git a/package.json b/package.json index 20f4cdb..81daaef 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,21 @@ { "name": "smartyx", - "version": "1.4.0", + "version": "2.0.0", "description": "nodeJS模板引擎,理念源自于PHP的smarty模板引擎", - "keywords": ["fivejs", "smarty", "template", "ejs", "jade"], - "author": "宇天 ", + "keywords": ["fivejs", "php", "smarty", "template", "ejs", "jade"], + "author": "yutent ", "repository": { "type": "git", - "url": "https://github.com/yutent/smarty.git" + "url": "https://github.com/bytedo/smarty.git" }, "dependencies": { - "es.shim": "^1.1.2", - "iofs": "^1.3.2" + "es.shim": "^2.0.0", + "iofs": "^2.0.0" + }, + "main": "index.js", + "exports": { + "require": "./index.js", + "import": "./index.mjs" }, - "devDependencies": {}, - "main": "lib/main.js", "license": "MIT" }