├── .npmignore ├── Gruntfile.js ├── README.md ├── demo ├── basic.html ├── compile.html ├── debug-syntax.html ├── debug.html ├── helper.html ├── include.html ├── index.html ├── no-escape.html ├── node-template-express.js ├── node-template.js ├── node-template │ ├── copyright.html │ ├── index.html │ └── public │ │ ├── footer.html │ │ ├── header.html │ │ └── logo.html ├── print.html └── template-native │ ├── basic.html │ ├── compile.html │ ├── debug-syntax.html │ ├── debug.html │ ├── helper.html │ ├── include.html │ ├── index.html │ ├── no-escape.html │ ├── print.html │ └── tag.html ├── dist ├── template-debug.js ├── template-native-debug.js ├── template-native.js └── template.js ├── doc ├── syntax-native.md └── syntax-simple.md ├── node ├── _node.js ├── template-native.js └── template.js ├── package.json ├── src ├── cache.js ├── compile.js ├── config.js ├── get.js ├── helper.js ├── intro.js ├── onerror.js ├── outro.js ├── render.js ├── renderFile.js ├── syntax.js ├── template.js └── utils.js └── test ├── js ├── baiduTemplate.js ├── doT.js ├── easytemplate.js ├── etpl.js ├── handlebars.js ├── highcharts.js ├── jquery-1.7.2.min.js ├── jquery.tmpl.js ├── juicer.js ├── kissy.js ├── mustache.js ├── qunit │ ├── qunit.css │ └── qunit.js ├── template.js ├── tmpl.js └── underscore.js ├── test-helper.html ├── test-native.html ├── test-node.js ├── test-speed.html ├── test-xss.html ├── test.html └── tpl └── index.html /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | demo 3 | doc -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | var sources_native = [ 4 | 'src/intro.js', 5 | 'src/template.js', 6 | 'src/config.js', 7 | 'src/cache.js', 8 | 'src/render.js', 9 | 'src/renderFile.js', 10 | 'src/get.js', 11 | 'src/utils.js', 12 | 'src/helper.js', 13 | 'src/onerror.js', 14 | 'src/compile.js', 15 | //<<<< 'src/syntax.js', 16 | 'src/outro.js' 17 | ]; 18 | 19 | var sources_simple = Array.apply(null, sources_native); 20 | sources_simple.splice(sources_native.length - 1, 0, 'src/syntax.js'); 21 | 22 | 23 | grunt.initConfig({ 24 | pkg: grunt.file.readJSON('package.json'), 25 | meta: { 26 | banner: '/*!<%= pkg.name %> - Template Engine | <%= pkg.homepage %>*/\n' 27 | }, 28 | concat: { 29 | options: { 30 | separator: '' 31 | }, 32 | 33 | 'native': { 34 | src: sources_native, 35 | dest: 'dist/template-native-debug.js' 36 | }, 37 | 38 | simple: { 39 | src: sources_simple, 40 | dest: 'dist/template-debug.js' 41 | } 42 | }, 43 | uglify: { 44 | options: { 45 | banner: '<%= meta.banner %>' 46 | }, 47 | 'native': { 48 | src: '<%= concat.native.dest %>', 49 | dest: 'dist/template-native.js' 50 | }, 51 | simple: { 52 | src: '<%= concat.simple.dest %>', 53 | dest: 'dist/template.js' 54 | } 55 | }, 56 | qunit: { 57 | files: ['test/**/*.html'] 58 | }, 59 | jshint: { 60 | files: [ 61 | 'dist/template-native.js', 62 | 'dist/template.js' 63 | ], 64 | options: { 65 | curly: true, 66 | eqeqeq: true, 67 | immed: true, 68 | latedef: true, 69 | newcap: true, 70 | noarg: true, 71 | sub: true, 72 | undef: true, 73 | boss: true, 74 | eqnull: true, 75 | browser: true 76 | }, 77 | globals: { 78 | console: true, 79 | define: true, 80 | global: true, 81 | module: true 82 | } 83 | }, 84 | watch: { 85 | files: '', 86 | tasks: 'lint qunit' 87 | } 88 | }); 89 | 90 | grunt.loadNpmTasks('grunt-contrib-uglify'); 91 | grunt.loadNpmTasks('grunt-contrib-jshint'); 92 | //grunt.loadNpmTasks('grunt-contrib-qunit'); 93 | //grunt.loadNpmTasks('grunt-contrib-watch'); 94 | grunt.loadNpmTasks('grunt-contrib-concat'); 95 | 96 | 97 | grunt.registerTask('default', ['concat', /*'jshint',*/ 'uglify']); 98 | 99 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # artTemplate-3.0 2 | 3 | 新一代 javascript 模板引擎 4 | 5 | ## 目录 6 | 7 | * [特性](#特性) 8 | * [快速上手](#快速上手) 9 | * [模板语法](#模板语法) 10 | * [下载](#下载) 11 | * [方法](#方法) 12 | * [NodeJS](#nodejs) 13 | * [使用预编译](#使用预编译) 14 | * [更新日志](#更新日志) 15 | * [授权协议](#授权协议) 16 | 17 | ## 特性 18 | 19 | 1. 性能卓越,执行速度通常是 Mustache 与 tmpl 的 20 多倍([性能测试](http://aui.github.com/artTemplate/test/test-speed.html)) 20 | 2. 支持运行时调试,可精确定位异常模板所在语句([演示](http://aui.github.io/artTemplate/demo/debug.html)) 21 | 3. 对 NodeJS Express 友好支持 22 | 4. 安全,默认对输出进行转义、在沙箱中运行编译后的代码(Node版本可以安全执行用户上传的模板) 23 | 5. 支持``include``语句 24 | 6. 可在浏览器端实现按路径加载模板([详情](#使用预编译)) 25 | 7. 支持预编译,可将模板转换成为非常精简的 js 文件 26 | 8. 模板语句简洁,无需前缀引用数据,有简洁版本与原生语法版本可选 27 | 9. 支持所有流行的浏览器 28 | 29 | ## 快速上手 30 | 31 | ### 编写模板 32 | 33 | 使用一个``type="text/html"``的``script``标签存放模板: 34 | 35 | 43 | 44 | ### 渲染模板 45 | 46 | var data = { 47 | title: '标签', 48 | list: ['文艺', '博客', '摄影', '电影', '民谣', '旅行', '吉他'] 49 | }; 50 | var html = template('test', data); 51 | document.getElementById('content').innerHTML = html; 52 | 53 | 54 | [演示](http://aui.github.com/artTemplate/demo/basic.html) 55 | 56 | ## 模板语法 57 | 58 | 有两个版本的模板语法可以选择。 59 | 60 | ### 简洁语法 61 | 62 | 推荐使用,语法简单实用,利于读写。 63 | 64 | {{if admin}} 65 | {{include 'admin_content'}} 66 | 67 | {{each list}} 68 |
{{$index}}. {{$value.user}}
69 | {{/each}} 70 | {{/if}} 71 | 72 | [查看语法与演示](https://github.com/aui/artTemplate/wiki/syntax:simple) 73 | 74 | ### 原生语法 75 | 76 | <%if (admin){%> 77 | <%include('admin_content')%> 78 | 79 | <%for (var i=0;i 80 |
<%=i%>. <%=list[i].user%>
81 | <%}%> 82 | <%}%> 83 | 84 | [查看语法与演示](https://github.com/aui/artTemplate/wiki/syntax:native) 85 | 86 | ## 下载 87 | 88 | * [template.js](https://raw.github.com/aui/artTemplate/master/dist/template.js) *(简洁语法版, 2.7kb)* 89 | * [template-native.js](https://raw.github.com/aui/artTemplate/master/dist/template-native.js) *(原生语法版, 2.3kb)* 90 | 91 | ## 方法 92 | 93 | ### template(id, data) 94 | 95 | 根据 id 渲染模板。内部会根据``document.getElementById(id)``查找模板。 96 | 97 | 如果没有 data 参数,那么将返回一渲染函数。 98 | 99 | ### template.``compile``(source, options) 100 | 101 | 将返回一个渲染函数。[演示](http://aui.github.com/artTemplate/demo/compile.html) 102 | 103 | ### template.``render``(source, options) 104 | 105 | 将返回渲染结果。 106 | 107 | ### template.``helper``(name, callback) 108 | 109 | 添加辅助方法。 110 | 111 | 例如时间格式器:[演示](http://aui.github.com/artTemplate/demo/helper.html) 112 | 113 | ### template.``config``(name, value) 114 | 115 | 更改引擎的默认配置。 116 | 117 | 字段 | 类型 | 默认值| 说明 118 | ------------ | ------------- | ------------ | ------------ 119 | openTag | String | ``'{{'`` | 逻辑语法开始标签 120 | closeTag | String | ``"}}"`` | 逻辑语法结束标签 121 | escape | Boolean | ``true`` | 是否编码输出 HTML 字符 122 | cache | Boolean | ``true`` | 是否开启缓存(依赖 options 的 filename 字段) 123 | compress | Boolean | ``false`` | 是否压缩 HTML 多余空白字符 124 | 125 | ## 使用预编译 126 | 127 | 可突破浏览器限制,让前端模板拥有后端模板一样的同步“文件”加载能力: 128 | 129 | 一、**按文件与目录组织模板** 130 | 131 | ``` 132 | template('tpl/home/main', data) 133 | ``` 134 | 135 | 二、**模板支持引入子模板** 136 | 137 | 138 | {{include '../public/header'}} 139 | 140 | ### 基于预编译: 141 | 142 | * 可将模板转换成为非常精简的 js 文件(不依赖引擎) 143 | * 使用同步模板加载接口 144 | * 支持多种 js 模块输出:AMD、CMD、CommonJS 145 | * 支持作为 GruntJS 插件构建 146 | * 前端模板可共享给 NodeJS 执行 147 | * 自动压缩打包模板 148 | 149 | 预编译工具:[TmodJS](http://github.com/aui/tmodjs/) 150 | 151 | ## NodeJS 152 | 153 | ### 安装 154 | 155 | npm install art-template 156 | 157 | ### 使用 158 | 159 | var template = require('art-template'); 160 | var data = {list: ["aui", "test"]}; 161 | 162 | var html = template(__dirname + '/index/main', data); 163 | 164 | ### 配置 165 | 166 | NodeJS 版本新增了如下默认配置: 167 | 168 | 字段 | 类型 | 默认值| 说明 169 | ------------ | ------------- | ------------ | ------------ 170 | base | String | ``''`` | 指定模板目录 171 | extname | String | ``'.html'`` | 指定模板后缀名 172 | encoding | String | ``'utf-8'`` | 指定模板编码 173 | 174 | 配置``base``指定模板目录可以缩短模板的路径,并且能够避免``include``语句越级访问任意路径引发安全隐患,例如: 175 | 176 | template.config('base', __dirname); 177 | var html = template('index/main', data) 178 | 179 | ### NodeJS + Express 180 | 181 | var template = require('art-template'); 182 | template.config('base', ''); 183 | template.config('extname', '.html'); 184 | app.engine('.html', template.__express); 185 | app.set('view engine', 'html'); 186 | //app.set('views', __dirname + '/views'); 187 | 188 | 运行 demo: 189 | 190 | node demo/node-template-express.js 191 | 192 | > 若使用 js 原生语法作为模板语法,请改用 ``require('art-template/node/template-native.js')`` 193 | 194 | ## 升级参考 195 | 196 | 为了适配 NodeJS express,artTemplate v3.0.0 接口有调整。 197 | 198 | ### 接口变更 199 | 200 | 1. 默认使用简洁语法 201 | 2. ``template.render()``方法的第一个参数不再是 id,而是模板字符串 202 | 3. 使用新的配置接口``template.config()``并且字段名有修改 203 | 4. ``template.compile()``方法不支持 id 参数 204 | 5. helper 方法不再强制原文输出,是否编码取决于模板语句 205 | 6. ``template.helpers`` 中的``$string``、``$escape``、``$each``已迁移到``template.utils``中 206 | 7. ``template()``方法不支持传入模板直接编译 207 | 208 | ### 升级方法 209 | 210 | 1. 如果想继续使用 js 原生语法作为模板语言,请使用 [template-native.js](https://raw.github.com/aui/artTemplate/master/dist/template-native.js) 211 | 2. 查找项目```template.render```替换为```template``` 212 | 3. 使用``template.config(name, value)``来替换以前的配置 213 | 4. ``template()``方法直接传入的模板改用``template.compile()``(v2初期版本) 214 | 215 | ## 更新日志 216 | 217 | ### v3.0.3 218 | 219 | 1. 解决``template.helper()``方法传入的数据被转成字符串的问题 #96 220 | 2. 解决``{{value || value2}}``被识别为管道语句的问题 #105 221 | 222 | ### v3.0.2 223 | 224 | 1. ~~解决管道语法必须使用空格分隔的问题~~ 225 | 226 | ### v3.0.1 227 | 228 | 1. 适配 express3.x 与 4.x,修复路径 BUG 229 | 230 | ### v3.0.0 231 | 232 | 1. 提供 NodeJS 专属版本,支持使用路径加载模板,并且模板的``include``语句也支持相对路径 233 | 2. 适配 [express](http://expressjs.com) 框架 234 | 3. 内置``print``语句支持传入多个参数 235 | 4. 支持全局缓存配置 236 | 5. 简洁语法版支持管道风格的 helper 调用,例如:``{{time | dateFormat:'yyyy年 MM月 dd日 hh:mm:ss'}}`` 237 | 238 | 当前版本接口有调整,请阅读 [升级参考](#升级参考) 239 | 240 | > artTemplate 预编译工具 [TmodJS](https://github.com/aui/tmodjs) 已更新 241 | 242 | ### v2.0.4 243 | 244 | 1. 修复低版本安卓浏览器编译后可能产生语法错误的问题(因为此版本浏览器 js 引擎存在 BUG) 245 | 246 | ### v2.0.3 247 | 248 | 1. 优化辅助方法性能 249 | 2. NodeJS 用户可以通过 npm 获取 artTemplate:``$ npm install art-template -g`` 250 | 3. 不转义输出语句推荐使用``<%=#value%>``(兼容 v2.0.3 版本之前使用的``<%==value%>``),而简版语法则可以使用``{{#value}}`` 251 | 4. 提供简版语法的合并版本 dist/[template-simple.js](https://raw.github.com/aui/artTemplate/master/dist/template-simple.js) 252 | 253 | ### v2.0.2 254 | 255 | 1. 优化自定义语法扩展,减少体积 256 | 2. [重要]为了最大化兼容第三方库,自定义语法扩展默认界定符修改为``{{``与``}}``。 257 | 3. 修复合并工具的BUG [#25](https://github.com/aui/artTemplate/issues/25) 258 | 4. 公开了内部缓存,可以通过``template.cache``访问到编译后的函数 259 | 5. 公开了辅助方法缓存,可以通过``template.helpers``访问到 260 | 6. 优化了调试信息 261 | 262 | ### v2.0.1 263 | 264 | 1. 修复模板变量静态分析的[BUG](https://github.com/aui/artTemplate/pull/22) 265 | 266 | ### v2.0 release 267 | 268 | 1. ~~编译工具更名为 atc,成为 artTemplate 的子项目单独维护:~~ 269 | 270 | ### v2.0 beta5 271 | 272 | 1. 修复编译工具可能存在重复依赖的问题。感谢 @warmhug 273 | 2. 修复预编译``include``内部实现可能产生上下文不一致的问题。感谢 @warmhug 274 | 3. 编译工具支持使用拖拽文件进行单独编译 275 | 276 | ### v2.0 beta4 277 | 278 | 1. 修复编译工具在压缩模板可能导致 HTML 意外截断的问题。感谢 @warmhug 279 | 2. 完善编译工具对``include``支持支持,可以支持不同目录之间模板嵌套 280 | 3. 修复编译工具没能正确处理自定义语法插件的辅助方法 281 | 282 | ### v2.0 beta1 283 | 284 | 1. 对非 String、Number 类型的数据不输出,而 Function 类型求值后输出。 285 | 2. 默认对 html 进行转义输出,原文输出可使用``<%==value%>``(备注:v2.0.3 推荐使用``<%=#value%>``),也可以关闭默认的转义功能``template.defaults.escape = false``。 286 | 3. 增加批处理工具支持把模板编译成不依赖模板引擎的 js 文件,可通过 RequireJS、SeaJS 等模块加载器进行异步加载。 287 | 288 | ## 授权协议 289 | 290 | Released under the MIT, BSD, and GPL Licenses 291 | 292 | ============ 293 | 294 | [所有演示例子](http://aui.github.com/artTemplate/demo/index.html) | [引擎原理](http://cdc.tencent.com/?p=5723) 295 | 296 | © tencent.com 297 | -------------------------------------------------------------------------------- /demo/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | basic-demo 6 | 7 | 8 | 9 | 10 |
11 | 23 | 24 | 33 | 34 | -------------------------------------------------------------------------------- /demo/compile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | compile-demo 6 | 7 | 8 | 9 | 10 |

在javascript中存放模板

11 |
12 | 26 | 27 | -------------------------------------------------------------------------------- /demo/debug-syntax.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | debug-demo 6 | 7 | 8 | 9 | 10 |

错误捕获(请打开控制台)

11 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | debug-demo 6 | 7 | 8 | 9 | 10 |

错误捕获(请打开控制台)

11 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /demo/helper.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | helper-demo 6 | 7 | 8 | 9 | 10 |

辅助方法

11 |
12 | 15 | 16 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /demo/include.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | include-demo 6 | 7 | 8 | 9 | 10 |
11 | 15 | 22 | 23 | 31 | 32 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | demo 6 | 7 | 8 | 9 |

演示

10 | 11 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo/no-escape.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | no escape-demo 6 | 7 | 8 | 9 | 10 |

不转义HTML

11 |
12 | 16 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /demo/node-template-express.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var template = require('../node/template.js'); 3 | 4 | 5 | var app = module.exports = express(); 6 | 7 | template.config('extname', '.html'); 8 | app.engine('.html', template.__express); 9 | app.set('view engine', 'html'); 10 | app.set('views', __dirname + '/node-template'); 11 | 12 | 13 | var demoData = { 14 | title: '国内要闻', 15 | time: (new Date).toString(), 16 | list: [ 17 | { 18 | title: '<油价>调整周期缩至10个工作日 无4%幅度限制', 19 | url: 'http://finance.qq.com/zt2013/2013yj/index.htm' 20 | }, 21 | { 22 | title: '明起汽油价格每吨下调310元 单价回归7元时代', 23 | url: 'http://finance.qq.com/a/20130326/007060.htm' 24 | }, 25 | { 26 | title: '广东副县长疑因抛弃情妇遭6女子围殴 纪检调查', 27 | url: 'http://news.qq.com/a/20130326/001254.htm' 28 | }, 29 | { 30 | title: '湖南27岁副县长回应质疑:父亲已不是领导', 31 | url: 'http://news.qq.com/a/20130326/000959.htm' 32 | }, 33 | { 34 | title: '朝军进入战斗工作状态 称随时准备导弹攻击美国', 35 | url: 'http://news.qq.com/a/20130326/001307.htm' 36 | } 37 | ] 38 | }; 39 | 40 | app.get('/', function(req, res){ 41 | res.render('./index', demoData); 42 | }); 43 | 44 | /* istanbul ignore next */ 45 | if (!module.parent) { 46 | app.listen(3000); 47 | console.log('Express started on port 3000'); 48 | } -------------------------------------------------------------------------------- /demo/node-template.js: -------------------------------------------------------------------------------- 1 | var template = require('../node/template.js'); 2 | //console.log(template); 3 | template.config('base', __dirname);// 设置模板根目录,默认为引擎所在目录 4 | template.config('compress', true);// 压缩输出 5 | 6 | var html = template('node-template/index', { 7 | title: '国内要闻', 8 | time: (new Date).toString(), 9 | list: [ 10 | { 11 | title: '<油价>调整周期缩至10个工作日 无4%幅度限制', 12 | url: 'http://finance.qq.com/zt2013/2013yj/index.htm' 13 | }, 14 | { 15 | title: '明起汽油价格每吨下调310元 单价回归7元时代', 16 | url: 'http://finance.qq.com/a/20130326/007060.htm' 17 | }, 18 | { 19 | title: '广东副县长疑因抛弃情妇遭6女子围殴 纪检调查', 20 | url: 'http://news.qq.com/a/20130326/001254.htm' 21 | }, 22 | { 23 | title: '湖南27岁副县长回应质疑:父亲已不是领导', 24 | url: 'http://news.qq.com/a/20130326/000959.htm' 25 | }, 26 | { 27 | title: '朝军进入战斗工作状态 称随时准备导弹攻击美国', 28 | url: 'http://news.qq.com/a/20130326/001307.htm' 29 | } 30 | ] 31 | }); 32 | 33 | 34 | console.log(html); 35 | //console.log(template.cache) -------------------------------------------------------------------------------- /demo/node-template/copyright.html: -------------------------------------------------------------------------------- 1 | (c) 2013 -------------------------------------------------------------------------------- /demo/node-template/index.html: -------------------------------------------------------------------------------- 1 | {{include './public/header'}} 2 | 3 |
4 |

{{title}}

5 | 10 |
11 | 12 | {{include './public/footer'}} -------------------------------------------------------------------------------- /demo/node-template/public/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/node-template/public/header.html: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /demo/node-template/public/logo.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 腾讯网 5 | 6 |

7 | -------------------------------------------------------------------------------- /demo/print.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | print-demo 6 | 7 | 8 | 9 | 10 |

print

11 | 14 | 15 | 28 | 29 | -------------------------------------------------------------------------------- /demo/template-native/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | basic-demo 6 | 7 | 8 | 9 | 10 |
11 | 23 | 24 | 33 | 34 | -------------------------------------------------------------------------------- /demo/template-native/compile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | compile-demo 6 | 7 | 8 | 9 | 10 |

在javascript中存放模板

11 |
12 | 26 | 27 | -------------------------------------------------------------------------------- /demo/template-native/debug-syntax.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | debug-demo 6 | 7 | 8 | 9 | 10 |

错误捕获(请打开控制台)

11 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /demo/template-native/debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | debug-demo 6 | 7 | 8 | 9 | 10 |

错误捕获(请打开控制台)

11 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /demo/template-native/helper.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | helper-demo 6 | 7 | 8 | 9 | 10 |

辅助方法

11 |
12 | 15 | 16 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /demo/template-native/include.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | include-demo 6 | 7 | 8 | 9 | 10 |
11 | 15 | 22 | 23 | 31 | 32 | -------------------------------------------------------------------------------- /demo/template-native/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | demo 6 | 7 | 8 | 9 |

演示

10 | 11 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/template-native/no-escape.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | no escape-demo 6 | 7 | 8 | 9 | 10 |

不转义HTML

11 |
12 | 16 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /demo/template-native/print.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | print-demo 6 | 7 | 8 | 9 | 10 |

print

11 | 15 | 16 | 29 | 30 | -------------------------------------------------------------------------------- /demo/template-native/tag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tag-demo 6 | 7 | 8 | 9 | 10 |

自定义界定符

11 | 24 | 25 | 41 | 42 | -------------------------------------------------------------------------------- /dist/template-debug.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * artTemplate - Template Engine 3 | * https://github.com/aui/artTemplate 4 | * Released under the MIT, BSD, and GPL Licenses 5 | */ 6 | 7 | !(function () { 8 | 9 | 10 | /** 11 | * 模板引擎 12 | * @name template 13 | * @param {String} 模板名 14 | * @param {Object, String} 数据。如果为字符串则编译并缓存编译结果 15 | * @return {String, Function} 渲染好的HTML字符串或者渲染方法 16 | */ 17 | var template = function (filename, content) { 18 | return typeof content === 'string' 19 | ? compile(content, { 20 | filename: filename 21 | }) 22 | : renderFile(filename, content); 23 | }; 24 | 25 | 26 | template.version = '3.0.0'; 27 | 28 | 29 | /** 30 | * 设置全局配置 31 | * @name template.config 32 | * @param {String} 名称 33 | * @param {Any} 值 34 | */ 35 | template.config = function (name, value) { 36 | defaults[name] = value; 37 | }; 38 | 39 | 40 | 41 | var defaults = template.defaults = { 42 | openTag: '<%', // 逻辑语法开始标签 43 | closeTag: '%>', // 逻辑语法结束标签 44 | escape: true, // 是否编码输出变量的 HTML 字符 45 | cache: true, // 是否开启缓存(依赖 options 的 filename 字段) 46 | compress: false, // 是否压缩输出 47 | parser: null // 自定义语法格式器 @see: template-syntax.js 48 | }; 49 | 50 | 51 | var cacheStore = template.cache = {}; 52 | 53 | 54 | /** 55 | * 渲染模板 56 | * @name template.render 57 | * @param {String} 模板 58 | * @param {Object} 数据 59 | * @return {String} 渲染好的字符串 60 | */ 61 | template.render = function (source, options) { 62 | return compile(source, options); 63 | }; 64 | 65 | 66 | /** 67 | * 渲染模板(根据模板名) 68 | * @name template.render 69 | * @param {String} 模板名 70 | * @param {Object} 数据 71 | * @return {String} 渲染好的字符串 72 | */ 73 | var renderFile = template.renderFile = function (filename, data) { 74 | var fn = template.get(filename) || showDebugInfo({ 75 | filename: filename, 76 | name: 'Render Error', 77 | message: 'Template not found' 78 | }); 79 | return data ? fn(data) : fn; 80 | }; 81 | 82 | 83 | /** 84 | * 获取编译缓存(可由外部重写此方法) 85 | * @param {String} 模板名 86 | * @param {Function} 编译好的函数 87 | */ 88 | template.get = function (filename) { 89 | 90 | var cache; 91 | 92 | if (cacheStore[filename]) { 93 | // 使用内存缓存 94 | cache = cacheStore[filename]; 95 | } else if (typeof document === 'object') { 96 | // 加载模板并编译 97 | var elem = document.getElementById(filename); 98 | 99 | if (elem) { 100 | var source = (elem.value || elem.innerHTML) 101 | .replace(/^\s*|\s*$/g, ''); 102 | cache = compile(source, { 103 | filename: filename 104 | }); 105 | } 106 | } 107 | 108 | return cache; 109 | }; 110 | 111 | 112 | var toString = function (value, type) { 113 | 114 | if (typeof value !== 'string') { 115 | 116 | type = typeof value; 117 | if (type === 'number') { 118 | value += ''; 119 | } else if (type === 'function') { 120 | value = toString(value.call(value)); 121 | } else { 122 | value = ''; 123 | } 124 | } 125 | 126 | return value; 127 | 128 | }; 129 | 130 | 131 | var escapeMap = { 132 | "<": "<", 133 | ">": ">", 134 | '"': """, 135 | "'": "'", 136 | "&": "&" 137 | }; 138 | 139 | 140 | var escapeFn = function (s) { 141 | return escapeMap[s]; 142 | }; 143 | 144 | var escapeHTML = function (content) { 145 | return toString(content) 146 | .replace(/&(?![\w#]+;)|[<>"']/g, escapeFn); 147 | }; 148 | 149 | 150 | var isArray = Array.isArray || function (obj) { 151 | return ({}).toString.call(obj) === '[object Array]'; 152 | }; 153 | 154 | 155 | var each = function (data, callback) { 156 | var i, len; 157 | if (isArray(data)) { 158 | for (i = 0, len = data.length; i < len; i++) { 159 | callback.call(data, data[i], i, data); 160 | } 161 | } else { 162 | for (i in data) { 163 | callback.call(data, data[i], i); 164 | } 165 | } 166 | }; 167 | 168 | 169 | var utils = template.utils = { 170 | 171 | $helpers: {}, 172 | 173 | $include: renderFile, 174 | 175 | $string: toString, 176 | 177 | $escape: escapeHTML, 178 | 179 | $each: each 180 | 181 | };/** 182 | * 添加模板辅助方法 183 | * @name template.helper 184 | * @param {String} 名称 185 | * @param {Function} 方法 186 | */ 187 | template.helper = function (name, helper) { 188 | helpers[name] = helper; 189 | }; 190 | 191 | var helpers = template.helpers = utils.$helpers; 192 | 193 | 194 | 195 | 196 | /** 197 | * 模板错误事件(可由外部重写此方法) 198 | * @name template.onerror 199 | * @event 200 | */ 201 | template.onerror = function (e) { 202 | var message = 'Template Error\n\n'; 203 | for (var name in e) { 204 | message += '<' + name + '>\n' + e[name] + '\n\n'; 205 | } 206 | 207 | if (typeof console === 'object') { 208 | console.error(message); 209 | } 210 | }; 211 | 212 | 213 | // 模板调试器 214 | var showDebugInfo = function (e) { 215 | 216 | template.onerror(e); 217 | 218 | return function () { 219 | return '{Template Error}'; 220 | }; 221 | }; 222 | 223 | 224 | /** 225 | * 编译模板 226 | * 2012-6-6 @TooBug: define 方法名改为 compile,与 Node Express 保持一致 227 | * @name template.compile 228 | * @param {String} 模板字符串 229 | * @param {Object} 编译选项 230 | * 231 | * - openTag {String} 232 | * - closeTag {String} 233 | * - filename {String} 234 | * - escape {Boolean} 235 | * - compress {Boolean} 236 | * - debug {Boolean} 237 | * - cache {Boolean} 238 | * - parser {Function} 239 | * 240 | * @return {Function} 渲染方法 241 | */ 242 | var compile = template.compile = function (source, options) { 243 | 244 | // 合并默认配置 245 | options = options || {}; 246 | for (var name in defaults) { 247 | if (options[name] === undefined) { 248 | options[name] = defaults[name]; 249 | } 250 | } 251 | 252 | 253 | var filename = options.filename; 254 | 255 | 256 | try { 257 | 258 | var Render = compiler(source, options); 259 | 260 | } catch (e) { 261 | 262 | e.filename = filename || 'anonymous'; 263 | e.name = 'Syntax Error'; 264 | 265 | return showDebugInfo(e); 266 | 267 | } 268 | 269 | 270 | // 对编译结果进行一次包装 271 | 272 | function render (data) { 273 | 274 | try { 275 | 276 | return new Render(data, filename) + ''; 277 | 278 | } catch (e) { 279 | 280 | // 运行时出错后自动开启调试模式重新编译 281 | if (!options.debug) { 282 | options.debug = true; 283 | return compile(source, options)(data); 284 | } 285 | 286 | return showDebugInfo(e)(); 287 | 288 | } 289 | 290 | } 291 | 292 | 293 | render.prototype = Render.prototype; 294 | render.toString = function () { 295 | return Render.toString(); 296 | }; 297 | 298 | 299 | if (filename && options.cache) { 300 | cacheStore[filename] = render; 301 | } 302 | 303 | 304 | return render; 305 | 306 | }; 307 | 308 | 309 | 310 | 311 | // 数组迭代 312 | var forEach = utils.$each; 313 | 314 | 315 | // 静态分析模板变量 316 | var KEYWORDS = 317 | // 关键字 318 | 'break,case,catch,continue,debugger,default,delete,do,else,false' 319 | + ',finally,for,function,if,in,instanceof,new,null,return,switch,this' 320 | + ',throw,true,try,typeof,var,void,while,with' 321 | 322 | // 保留字 323 | + ',abstract,boolean,byte,char,class,const,double,enum,export,extends' 324 | + ',final,float,goto,implements,import,int,interface,long,native' 325 | + ',package,private,protected,public,short,static,super,synchronized' 326 | + ',throws,transient,volatile' 327 | 328 | // ECMA 5 - use strict 329 | + ',arguments,let,yield' 330 | 331 | + ',undefined'; 332 | 333 | var REMOVE_RE = /\/\*[\w\W]*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|"(?:[^"\\]|\\[\w\W])*"|'(?:[^'\\]|\\[\w\W])*'|\s*\.\s*[$\w\.]+/g; 334 | var SPLIT_RE = /[^\w$]+/g; 335 | var KEYWORDS_RE = new RegExp(["\\b" + KEYWORDS.replace(/,/g, '\\b|\\b') + "\\b"].join('|'), 'g'); 336 | var NUMBER_RE = /^\d[^,]*|,\d[^,]*/g; 337 | var BOUNDARY_RE = /^,+|,+$/g; 338 | var SPLIT2_RE = /^$|,+/; 339 | 340 | 341 | // 获取变量 342 | function getVariable (code) { 343 | return code 344 | .replace(REMOVE_RE, '') 345 | .replace(SPLIT_RE, ',') 346 | .replace(KEYWORDS_RE, '') 347 | .replace(NUMBER_RE, '') 348 | .replace(BOUNDARY_RE, '') 349 | .split(SPLIT2_RE); 350 | }; 351 | 352 | 353 | // 字符串转义 354 | function stringify (code) { 355 | return "'" + code 356 | // 单引号与反斜杠转义 357 | .replace(/('|\\)/g, '\\$1') 358 | // 换行符转义(windows + linux) 359 | .replace(/\r/g, '\\r') 360 | .replace(/\n/g, '\\n') + "'"; 361 | } 362 | 363 | 364 | function compiler (source, options) { 365 | 366 | var debug = options.debug; 367 | var openTag = options.openTag; 368 | var closeTag = options.closeTag; 369 | var parser = options.parser; 370 | var compress = options.compress; 371 | var escape = options.escape; 372 | 373 | 374 | 375 | var line = 1; 376 | var uniq = {$data:1,$filename:1,$utils:1,$helpers:1,$out:1,$line:1}; 377 | 378 | 379 | 380 | var isNewEngine = ''.trim;// '__proto__' in {} 381 | var replaces = isNewEngine 382 | ? ["$out='';", "$out+=", ";", "$out"] 383 | : ["$out=[];", "$out.push(", ");", "$out.join('')"]; 384 | 385 | var concat = isNewEngine 386 | ? "$out+=text;return $out;" 387 | : "$out.push(text);"; 388 | 389 | var print = "function(){" 390 | + "var text=''.concat.apply('',arguments);" 391 | + concat 392 | + "}"; 393 | 394 | var include = "function(filename,data){" 395 | + "data=data||$data;" 396 | + "var text=$utils.$include(filename,data,$filename);" 397 | + concat 398 | + "}"; 399 | 400 | var headerCode = "'use strict';" 401 | + "var $utils=this,$helpers=$utils.$helpers," 402 | + (debug ? "$line=0," : ""); 403 | 404 | var mainCode = replaces[0]; 405 | 406 | var footerCode = "return new String(" + replaces[3] + ");" 407 | 408 | // html与逻辑语法分离 409 | forEach(source.split(openTag), function (code) { 410 | code = code.split(closeTag); 411 | 412 | var $0 = code[0]; 413 | var $1 = code[1]; 414 | 415 | // code: [html] 416 | if (code.length === 1) { 417 | 418 | mainCode += html($0); 419 | 420 | // code: [logic, html] 421 | } else { 422 | 423 | mainCode += logic($0); 424 | 425 | if ($1) { 426 | mainCode += html($1); 427 | } 428 | } 429 | 430 | 431 | }); 432 | 433 | var code = headerCode + mainCode + footerCode; 434 | 435 | // 调试语句 436 | if (debug) { 437 | code = "try{" + code + "}catch(e){" 438 | + "throw {" 439 | + "filename:$filename," 440 | + "name:'Render Error'," 441 | + "message:e.message," 442 | + "line:$line," 443 | + "source:" + stringify(source) 444 | + ".split(/\\n/)[$line-1].replace(/^\\s+/,'')" 445 | + "};" 446 | + "}"; 447 | } 448 | 449 | 450 | 451 | try { 452 | 453 | 454 | var Render = new Function("$data", "$filename", code); 455 | Render.prototype = utils; 456 | 457 | return Render; 458 | 459 | } catch (e) { 460 | e.temp = "function anonymous($data,$filename) {" + code + "}"; 461 | throw e; 462 | } 463 | 464 | 465 | 466 | 467 | // 处理 HTML 语句 468 | function html (code) { 469 | 470 | // 记录行号 471 | line += code.split(/\n/).length - 1; 472 | 473 | // 压缩多余空白与注释 474 | if (compress) { 475 | code = code 476 | .replace(/\s+/g, ' ') 477 | .replace(//g, ''); 478 | } 479 | 480 | if (code) { 481 | code = replaces[1] + stringify(code) + replaces[2] + "\n"; 482 | } 483 | 484 | return code; 485 | } 486 | 487 | 488 | // 处理逻辑语句 489 | function logic (code) { 490 | 491 | var thisLine = line; 492 | 493 | if (parser) { 494 | 495 | // 语法转换插件钩子 496 | code = parser(code, options); 497 | 498 | } else if (debug) { 499 | 500 | // 记录行号 501 | code = code.replace(/\n/g, function () { 502 | line ++; 503 | return "$line=" + line + ";"; 504 | }); 505 | 506 | } 507 | 508 | 509 | // 输出语句. 编码: <%=value%> 不编码:<%=#value%> 510 | // <%=#value%> 等同 v2.0.3 之前的 <%==value%> 511 | if (code.indexOf('=') === 0) { 512 | 513 | var escapeSyntax = escape && !/^=[=#]/.test(code); 514 | 515 | code = code.replace(/^=[=#]?|[\s;]*$/g, ''); 516 | 517 | // 对内容编码 518 | if (escapeSyntax) { 519 | 520 | var name = code.replace(/\s*\([^\)]+\)/, ''); 521 | 522 | // 排除 utils.* | include | print 523 | 524 | if (!utils[name] && !/^(include|print)$/.test(name)) { 525 | code = "$escape(" + code + ")"; 526 | } 527 | 528 | // 不编码 529 | } else { 530 | code = "$string(" + code + ")"; 531 | } 532 | 533 | 534 | code = replaces[1] + code + replaces[2]; 535 | 536 | } 537 | 538 | if (debug) { 539 | code = "$line=" + thisLine + ";" + code; 540 | } 541 | 542 | // 提取模板中的变量名 543 | forEach(getVariable(code), function (name) { 544 | 545 | // name 值可能为空,在安卓低版本浏览器下 546 | if (!name || uniq[name]) { 547 | return; 548 | } 549 | 550 | var value; 551 | 552 | // 声明模板变量 553 | // 赋值优先级: 554 | // [include, print] > utils > helpers > data 555 | if (name === 'print') { 556 | 557 | value = print; 558 | 559 | } else if (name === 'include') { 560 | 561 | value = include; 562 | 563 | } else if (utils[name]) { 564 | 565 | value = "$utils." + name; 566 | 567 | } else if (helpers[name]) { 568 | 569 | value = "$helpers." + name; 570 | 571 | } else { 572 | 573 | value = "$data." + name; 574 | } 575 | 576 | headerCode += name + "=" + value + ","; 577 | uniq[name] = true; 578 | 579 | 580 | }); 581 | 582 | return code + "\n"; 583 | } 584 | 585 | 586 | }; 587 | 588 | 589 | 590 | // 定义模板引擎的语法 591 | 592 | 593 | defaults.openTag = '{{'; 594 | defaults.closeTag = '}}'; 595 | 596 | 597 | var filtered = function (js, filter) { 598 | var parts = filter.split(':'); 599 | var name = parts.shift(); 600 | var args = parts.join(':') || ''; 601 | 602 | if (args) { 603 | args = ', ' + args; 604 | } 605 | 606 | return '$helpers.' + name + '(' + js + args + ')'; 607 | } 608 | 609 | 610 | defaults.parser = function (code, options) { 611 | 612 | // var match = code.match(/([\w\$]*)(\b.*)/); 613 | // var key = match[1]; 614 | // var args = match[2]; 615 | // var split = args.split(' '); 616 | // split.shift(); 617 | 618 | code = code.replace(/^\s/, ''); 619 | 620 | var split = code.split(' '); 621 | var key = split.shift(); 622 | var args = split.join(' '); 623 | 624 | 625 | 626 | switch (key) { 627 | 628 | case 'if': 629 | 630 | code = 'if(' + args + '){'; 631 | break; 632 | 633 | case 'else': 634 | 635 | if (split.shift() === 'if') { 636 | split = ' if(' + split.join(' ') + ')'; 637 | } else { 638 | split = ''; 639 | } 640 | 641 | code = '}else' + split + '{'; 642 | break; 643 | 644 | case '/if': 645 | 646 | code = '}'; 647 | break; 648 | 649 | case 'each': 650 | 651 | var object = split[0] || '$data'; 652 | var as = split[1] || 'as'; 653 | var value = split[2] || '$value'; 654 | var index = split[3] || '$index'; 655 | 656 | var param = value + ',' + index; 657 | 658 | if (as !== 'as') { 659 | object = '[]'; 660 | } 661 | 662 | code = '$each(' + object + ',function(' + param + '){'; 663 | break; 664 | 665 | case '/each': 666 | 667 | code = '});'; 668 | break; 669 | 670 | case 'echo': 671 | 672 | code = 'print(' + args + ');'; 673 | break; 674 | 675 | case 'print': 676 | case 'include': 677 | 678 | code = key + '(' + split.join(',') + ');'; 679 | break; 680 | 681 | default: 682 | 683 | // 过滤器(辅助方法) 684 | // {{value | filterA:'abcd' | filterB}} 685 | // >>> $helpers.filterB($helpers.filterA(value, 'abcd')) 686 | // TODO: {{ddd||aaa}} 不包含空格 687 | if (/^\s*\|\s*[\w\$]/.test(args)) { 688 | 689 | var escape = true; 690 | 691 | // {{#value | link}} 692 | if (code.indexOf('#') === 0) { 693 | code = code.substr(1); 694 | escape = false; 695 | } 696 | 697 | var i = 0; 698 | var array = code.split('|'); 699 | var len = array.length; 700 | var val = array[i++]; 701 | 702 | for (; i < len; i ++) { 703 | val = filtered(val, array[i]); 704 | } 705 | 706 | code = (escape ? '=' : '=#') + val; 707 | 708 | // 即将弃用 {{helperName value}} 709 | } else if (template.helpers[key]) { 710 | 711 | code = '=#' + key + '(' + split.join(',') + ');'; 712 | 713 | // 内容直接输出 {{value}} 714 | } else { 715 | 716 | code = '=' + code; 717 | } 718 | 719 | break; 720 | } 721 | 722 | 723 | return code; 724 | }; 725 | 726 | 727 | 728 | // RequireJS && SeaJS 729 | if (typeof define === 'function') { 730 | define(function() { 731 | return template; 732 | }); 733 | 734 | // NodeJS 735 | } else if (typeof exports !== 'undefined') { 736 | module.exports = template; 737 | } else { 738 | this.template = template; 739 | } 740 | 741 | })(); -------------------------------------------------------------------------------- /dist/template-native-debug.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * artTemplate - Template Engine 3 | * https://github.com/aui/artTemplate 4 | * Released under the MIT, BSD, and GPL Licenses 5 | */ 6 | 7 | !(function () { 8 | 9 | 10 | /** 11 | * 模板引擎 12 | * @name template 13 | * @param {String} 模板名 14 | * @param {Object, String} 数据。如果为字符串则编译并缓存编译结果 15 | * @return {String, Function} 渲染好的HTML字符串或者渲染方法 16 | */ 17 | var template = function (filename, content) { 18 | return typeof content === 'string' 19 | ? compile(content, { 20 | filename: filename 21 | }) 22 | : renderFile(filename, content); 23 | }; 24 | 25 | 26 | template.version = '3.0.0'; 27 | 28 | 29 | /** 30 | * 设置全局配置 31 | * @name template.config 32 | * @param {String} 名称 33 | * @param {Any} 值 34 | */ 35 | template.config = function (name, value) { 36 | defaults[name] = value; 37 | }; 38 | 39 | 40 | 41 | var defaults = template.defaults = { 42 | openTag: '<%', // 逻辑语法开始标签 43 | closeTag: '%>', // 逻辑语法结束标签 44 | escape: true, // 是否编码输出变量的 HTML 字符 45 | cache: true, // 是否开启缓存(依赖 options 的 filename 字段) 46 | compress: false, // 是否压缩输出 47 | parser: null // 自定义语法格式器 @see: template-syntax.js 48 | }; 49 | 50 | 51 | var cacheStore = template.cache = {}; 52 | 53 | 54 | /** 55 | * 渲染模板 56 | * @name template.render 57 | * @param {String} 模板 58 | * @param {Object} 数据 59 | * @return {String} 渲染好的字符串 60 | */ 61 | template.render = function (source, options) { 62 | return compile(source, options); 63 | }; 64 | 65 | 66 | /** 67 | * 渲染模板(根据模板名) 68 | * @name template.render 69 | * @param {String} 模板名 70 | * @param {Object} 数据 71 | * @return {String} 渲染好的字符串 72 | */ 73 | var renderFile = template.renderFile = function (filename, data) { 74 | var fn = template.get(filename) || showDebugInfo({ 75 | filename: filename, 76 | name: 'Render Error', 77 | message: 'Template not found' 78 | }); 79 | return data ? fn(data) : fn; 80 | }; 81 | 82 | 83 | /** 84 | * 获取编译缓存(可由外部重写此方法) 85 | * @param {String} 模板名 86 | * @param {Function} 编译好的函数 87 | */ 88 | template.get = function (filename) { 89 | 90 | var cache; 91 | 92 | if (cacheStore[filename]) { 93 | // 使用内存缓存 94 | cache = cacheStore[filename]; 95 | } else if (typeof document === 'object') { 96 | // 加载模板并编译 97 | var elem = document.getElementById(filename); 98 | 99 | if (elem) { 100 | var source = (elem.value || elem.innerHTML) 101 | .replace(/^\s*|\s*$/g, ''); 102 | cache = compile(source, { 103 | filename: filename 104 | }); 105 | } 106 | } 107 | 108 | return cache; 109 | }; 110 | 111 | 112 | var toString = function (value, type) { 113 | 114 | if (typeof value !== 'string') { 115 | 116 | type = typeof value; 117 | if (type === 'number') { 118 | value += ''; 119 | } else if (type === 'function') { 120 | value = toString(value.call(value)); 121 | } else { 122 | value = ''; 123 | } 124 | } 125 | 126 | return value; 127 | 128 | }; 129 | 130 | 131 | var escapeMap = { 132 | "<": "<", 133 | ">": ">", 134 | '"': """, 135 | "'": "'", 136 | "&": "&" 137 | }; 138 | 139 | 140 | var escapeFn = function (s) { 141 | return escapeMap[s]; 142 | }; 143 | 144 | var escapeHTML = function (content) { 145 | return toString(content) 146 | .replace(/&(?![\w#]+;)|[<>"']/g, escapeFn); 147 | }; 148 | 149 | 150 | var isArray = Array.isArray || function (obj) { 151 | return ({}).toString.call(obj) === '[object Array]'; 152 | }; 153 | 154 | 155 | var each = function (data, callback) { 156 | var i, len; 157 | if (isArray(data)) { 158 | for (i = 0, len = data.length; i < len; i++) { 159 | callback.call(data, data[i], i, data); 160 | } 161 | } else { 162 | for (i in data) { 163 | callback.call(data, data[i], i); 164 | } 165 | } 166 | }; 167 | 168 | 169 | var utils = template.utils = { 170 | 171 | $helpers: {}, 172 | 173 | $include: renderFile, 174 | 175 | $string: toString, 176 | 177 | $escape: escapeHTML, 178 | 179 | $each: each 180 | 181 | };/** 182 | * 添加模板辅助方法 183 | * @name template.helper 184 | * @param {String} 名称 185 | * @param {Function} 方法 186 | */ 187 | template.helper = function (name, helper) { 188 | helpers[name] = helper; 189 | }; 190 | 191 | var helpers = template.helpers = utils.$helpers; 192 | 193 | 194 | 195 | 196 | /** 197 | * 模板错误事件(可由外部重写此方法) 198 | * @name template.onerror 199 | * @event 200 | */ 201 | template.onerror = function (e) { 202 | var message = 'Template Error\n\n'; 203 | for (var name in e) { 204 | message += '<' + name + '>\n' + e[name] + '\n\n'; 205 | } 206 | 207 | if (typeof console === 'object') { 208 | console.error(message); 209 | } 210 | }; 211 | 212 | 213 | // 模板调试器 214 | var showDebugInfo = function (e) { 215 | 216 | template.onerror(e); 217 | 218 | return function () { 219 | return '{Template Error}'; 220 | }; 221 | }; 222 | 223 | 224 | /** 225 | * 编译模板 226 | * 2012-6-6 @TooBug: define 方法名改为 compile,与 Node Express 保持一致 227 | * @name template.compile 228 | * @param {String} 模板字符串 229 | * @param {Object} 编译选项 230 | * 231 | * - openTag {String} 232 | * - closeTag {String} 233 | * - filename {String} 234 | * - escape {Boolean} 235 | * - compress {Boolean} 236 | * - debug {Boolean} 237 | * - cache {Boolean} 238 | * - parser {Function} 239 | * 240 | * @return {Function} 渲染方法 241 | */ 242 | var compile = template.compile = function (source, options) { 243 | 244 | // 合并默认配置 245 | options = options || {}; 246 | for (var name in defaults) { 247 | if (options[name] === undefined) { 248 | options[name] = defaults[name]; 249 | } 250 | } 251 | 252 | 253 | var filename = options.filename; 254 | 255 | 256 | try { 257 | 258 | var Render = compiler(source, options); 259 | 260 | } catch (e) { 261 | 262 | e.filename = filename || 'anonymous'; 263 | e.name = 'Syntax Error'; 264 | 265 | return showDebugInfo(e); 266 | 267 | } 268 | 269 | 270 | // 对编译结果进行一次包装 271 | 272 | function render (data) { 273 | 274 | try { 275 | 276 | return new Render(data, filename) + ''; 277 | 278 | } catch (e) { 279 | 280 | // 运行时出错后自动开启调试模式重新编译 281 | if (!options.debug) { 282 | options.debug = true; 283 | return compile(source, options)(data); 284 | } 285 | 286 | return showDebugInfo(e)(); 287 | 288 | } 289 | 290 | } 291 | 292 | 293 | render.prototype = Render.prototype; 294 | render.toString = function () { 295 | return Render.toString(); 296 | }; 297 | 298 | 299 | if (filename && options.cache) { 300 | cacheStore[filename] = render; 301 | } 302 | 303 | 304 | return render; 305 | 306 | }; 307 | 308 | 309 | 310 | 311 | // 数组迭代 312 | var forEach = utils.$each; 313 | 314 | 315 | // 静态分析模板变量 316 | var KEYWORDS = 317 | // 关键字 318 | 'break,case,catch,continue,debugger,default,delete,do,else,false' 319 | + ',finally,for,function,if,in,instanceof,new,null,return,switch,this' 320 | + ',throw,true,try,typeof,var,void,while,with' 321 | 322 | // 保留字 323 | + ',abstract,boolean,byte,char,class,const,double,enum,export,extends' 324 | + ',final,float,goto,implements,import,int,interface,long,native' 325 | + ',package,private,protected,public,short,static,super,synchronized' 326 | + ',throws,transient,volatile' 327 | 328 | // ECMA 5 - use strict 329 | + ',arguments,let,yield' 330 | 331 | + ',undefined'; 332 | 333 | var REMOVE_RE = /\/\*[\w\W]*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|"(?:[^"\\]|\\[\w\W])*"|'(?:[^'\\]|\\[\w\W])*'|\s*\.\s*[$\w\.]+/g; 334 | var SPLIT_RE = /[^\w$]+/g; 335 | var KEYWORDS_RE = new RegExp(["\\b" + KEYWORDS.replace(/,/g, '\\b|\\b') + "\\b"].join('|'), 'g'); 336 | var NUMBER_RE = /^\d[^,]*|,\d[^,]*/g; 337 | var BOUNDARY_RE = /^,+|,+$/g; 338 | var SPLIT2_RE = /^$|,+/; 339 | 340 | 341 | // 获取变量 342 | function getVariable (code) { 343 | return code 344 | .replace(REMOVE_RE, '') 345 | .replace(SPLIT_RE, ',') 346 | .replace(KEYWORDS_RE, '') 347 | .replace(NUMBER_RE, '') 348 | .replace(BOUNDARY_RE, '') 349 | .split(SPLIT2_RE); 350 | }; 351 | 352 | 353 | // 字符串转义 354 | function stringify (code) { 355 | return "'" + code 356 | // 单引号与反斜杠转义 357 | .replace(/('|\\)/g, '\\$1') 358 | // 换行符转义(windows + linux) 359 | .replace(/\r/g, '\\r') 360 | .replace(/\n/g, '\\n') + "'"; 361 | } 362 | 363 | 364 | function compiler (source, options) { 365 | 366 | var debug = options.debug; 367 | var openTag = options.openTag; 368 | var closeTag = options.closeTag; 369 | var parser = options.parser; 370 | var compress = options.compress; 371 | var escape = options.escape; 372 | 373 | 374 | 375 | var line = 1; 376 | var uniq = {$data:1,$filename:1,$utils:1,$helpers:1,$out:1,$line:1}; 377 | 378 | 379 | 380 | var isNewEngine = ''.trim;// '__proto__' in {} 381 | var replaces = isNewEngine 382 | ? ["$out='';", "$out+=", ";", "$out"] 383 | : ["$out=[];", "$out.push(", ");", "$out.join('')"]; 384 | 385 | var concat = isNewEngine 386 | ? "$out+=text;return $out;" 387 | : "$out.push(text);"; 388 | 389 | var print = "function(){" 390 | + "var text=''.concat.apply('',arguments);" 391 | + concat 392 | + "}"; 393 | 394 | var include = "function(filename,data){" 395 | + "data=data||$data;" 396 | + "var text=$utils.$include(filename,data,$filename);" 397 | + concat 398 | + "}"; 399 | 400 | var headerCode = "'use strict';" 401 | + "var $utils=this,$helpers=$utils.$helpers," 402 | + (debug ? "$line=0," : ""); 403 | 404 | var mainCode = replaces[0]; 405 | 406 | var footerCode = "return new String(" + replaces[3] + ");" 407 | 408 | // html与逻辑语法分离 409 | forEach(source.split(openTag), function (code) { 410 | code = code.split(closeTag); 411 | 412 | var $0 = code[0]; 413 | var $1 = code[1]; 414 | 415 | // code: [html] 416 | if (code.length === 1) { 417 | 418 | mainCode += html($0); 419 | 420 | // code: [logic, html] 421 | } else { 422 | 423 | mainCode += logic($0); 424 | 425 | if ($1) { 426 | mainCode += html($1); 427 | } 428 | } 429 | 430 | 431 | }); 432 | 433 | var code = headerCode + mainCode + footerCode; 434 | 435 | // 调试语句 436 | if (debug) { 437 | code = "try{" + code + "}catch(e){" 438 | + "throw {" 439 | + "filename:$filename," 440 | + "name:'Render Error'," 441 | + "message:e.message," 442 | + "line:$line," 443 | + "source:" + stringify(source) 444 | + ".split(/\\n/)[$line-1].replace(/^\\s+/,'')" 445 | + "};" 446 | + "}"; 447 | } 448 | 449 | 450 | 451 | try { 452 | 453 | 454 | var Render = new Function("$data", "$filename", code); 455 | Render.prototype = utils; 456 | 457 | return Render; 458 | 459 | } catch (e) { 460 | e.temp = "function anonymous($data,$filename) {" + code + "}"; 461 | throw e; 462 | } 463 | 464 | 465 | 466 | 467 | // 处理 HTML 语句 468 | function html (code) { 469 | 470 | // 记录行号 471 | line += code.split(/\n/).length - 1; 472 | 473 | // 压缩多余空白与注释 474 | if (compress) { 475 | code = code 476 | .replace(/\s+/g, ' ') 477 | .replace(//g, ''); 478 | } 479 | 480 | if (code) { 481 | code = replaces[1] + stringify(code) + replaces[2] + "\n"; 482 | } 483 | 484 | return code; 485 | } 486 | 487 | 488 | // 处理逻辑语句 489 | function logic (code) { 490 | 491 | var thisLine = line; 492 | 493 | if (parser) { 494 | 495 | // 语法转换插件钩子 496 | code = parser(code, options); 497 | 498 | } else if (debug) { 499 | 500 | // 记录行号 501 | code = code.replace(/\n/g, function () { 502 | line ++; 503 | return "$line=" + line + ";"; 504 | }); 505 | 506 | } 507 | 508 | 509 | // 输出语句. 编码: <%=value%> 不编码:<%=#value%> 510 | // <%=#value%> 等同 v2.0.3 之前的 <%==value%> 511 | if (code.indexOf('=') === 0) { 512 | 513 | var escapeSyntax = escape && !/^=[=#]/.test(code); 514 | 515 | code = code.replace(/^=[=#]?|[\s;]*$/g, ''); 516 | 517 | // 对内容编码 518 | if (escapeSyntax) { 519 | 520 | var name = code.replace(/\s*\([^\)]+\)/, ''); 521 | 522 | // 排除 utils.* | include | print 523 | 524 | if (!utils[name] && !/^(include|print)$/.test(name)) { 525 | code = "$escape(" + code + ")"; 526 | } 527 | 528 | // 不编码 529 | } else { 530 | code = "$string(" + code + ")"; 531 | } 532 | 533 | 534 | code = replaces[1] + code + replaces[2]; 535 | 536 | } 537 | 538 | if (debug) { 539 | code = "$line=" + thisLine + ";" + code; 540 | } 541 | 542 | // 提取模板中的变量名 543 | forEach(getVariable(code), function (name) { 544 | 545 | // name 值可能为空,在安卓低版本浏览器下 546 | if (!name || uniq[name]) { 547 | return; 548 | } 549 | 550 | var value; 551 | 552 | // 声明模板变量 553 | // 赋值优先级: 554 | // [include, print] > utils > helpers > data 555 | if (name === 'print') { 556 | 557 | value = print; 558 | 559 | } else if (name === 'include') { 560 | 561 | value = include; 562 | 563 | } else if (utils[name]) { 564 | 565 | value = "$utils." + name; 566 | 567 | } else if (helpers[name]) { 568 | 569 | value = "$helpers." + name; 570 | 571 | } else { 572 | 573 | value = "$data." + name; 574 | } 575 | 576 | headerCode += name + "=" + value + ","; 577 | uniq[name] = true; 578 | 579 | 580 | }); 581 | 582 | return code + "\n"; 583 | } 584 | 585 | 586 | }; 587 | 588 | 589 | 590 | 591 | // RequireJS && SeaJS 592 | if (typeof define === 'function') { 593 | define(function() { 594 | return template; 595 | }); 596 | 597 | // NodeJS 598 | } else if (typeof exports !== 'undefined') { 599 | module.exports = template; 600 | } else { 601 | this.template = template; 602 | } 603 | 604 | })(); -------------------------------------------------------------------------------- /dist/template-native.js: -------------------------------------------------------------------------------- 1 | /*!art-template - Template Engine | http://aui.github.com/artTemplate/*/ 2 | !function(){function a(a){return a.replace(t,"").replace(u,",").replace(v,"").replace(w,"").replace(x,"").split(y)}function b(a){return"'"+a.replace(/('|\\)/g,"\\$1").replace(/\r/g,"\\r").replace(/\n/g,"\\n")+"'"}function c(c,d){function e(a){return m+=a.split(/\n/).length-1,k&&(a=a.replace(/\s+/g," ").replace(//g,"")),a&&(a=s[1]+b(a)+s[2]+"\n"),a}function f(b){var c=m;if(j?b=j(b,d):g&&(b=b.replace(/\n/g,function(){return m++,"$line="+m+";"})),0===b.indexOf("=")){var e=l&&!/^=[=#]/.test(b);if(b=b.replace(/^=[=#]?|[\s;]*$/g,""),e){var f=b.replace(/\s*\([^\)]+\)/,"");n[f]||/^(include|print)$/.test(f)||(b="$escape("+b+")")}else b="$string("+b+")";b=s[1]+b+s[2]}return g&&(b="$line="+c+";"+b),r(a(b),function(a){if(a&&!p[a]){var b;b="print"===a?u:"include"===a?v:n[a]?"$utils."+a:o[a]?"$helpers."+a:"$data."+a,w+=a+"="+b+",",p[a]=!0}}),b+"\n"}var g=d.debug,h=d.openTag,i=d.closeTag,j=d.parser,k=d.compress,l=d.escape,m=1,p={$data:1,$filename:1,$utils:1,$helpers:1,$out:1,$line:1},q="".trim,s=q?["$out='';","$out+=",";","$out"]:["$out=[];","$out.push(",");","$out.join('')"],t=q?"$out+=text;return $out;":"$out.push(text);",u="function(){var text=''.concat.apply('',arguments);"+t+"}",v="function(filename,data){data=data||$data;var text=$utils.$include(filename,data,$filename);"+t+"}",w="'use strict';var $utils=this,$helpers=$utils.$helpers,"+(g?"$line=0,":""),x=s[0],y="return new String("+s[3]+");";r(c.split(h),function(a){a=a.split(i);var b=a[0],c=a[1];1===a.length?x+=e(b):(x+=f(b),c&&(x+=e(c)))});var z=w+x+y;g&&(z="try{"+z+"}catch(e){throw {filename:$filename,name:'Render Error',message:e.message,line:$line,source:"+b(c)+".split(/\\n/)[$line-1].replace(/^\\s+/,'')};}");try{var A=new Function("$data","$filename",z);return A.prototype=n,A}catch(B){throw B.temp="function anonymous($data,$filename) {"+z+"}",B}}var d=function(a,b){return"string"==typeof b?q(b,{filename:a}):g(a,b)};d.version="3.0.0",d.config=function(a,b){e[a]=b};var e=d.defaults={openTag:"<%",closeTag:"%>",escape:!0,cache:!0,compress:!1,parser:null},f=d.cache={};d.render=function(a,b){return q(a,b)};var g=d.renderFile=function(a,b){var c=d.get(a)||p({filename:a,name:"Render Error",message:"Template not found"});return b?c(b):c};d.get=function(a){var b;if(f[a])b=f[a];else if("object"==typeof document){var c=document.getElementById(a);if(c){var d=(c.value||c.innerHTML).replace(/^\s*|\s*$/g,"");b=q(d,{filename:a})}}return b};var h=function(a,b){return"string"!=typeof a&&(b=typeof a,"number"===b?a+="":a="function"===b?h(a.call(a)):""),a},i={"<":"<",">":">",'"':""","'":"'","&":"&"},j=function(a){return i[a]},k=function(a){return h(a).replace(/&(?![\w#]+;)|[<>"']/g,j)},l=Array.isArray||function(a){return"[object Array]"==={}.toString.call(a)},m=function(a,b){var c,d;if(l(a))for(c=0,d=a.length;d>c;c++)b.call(a,a[c],c,a);else for(c in a)b.call(a,a[c],c)},n=d.utils={$helpers:{},$include:g,$string:h,$escape:k,$each:m};d.helper=function(a,b){o[a]=b};var o=d.helpers=n.$helpers;d.onerror=function(a){var b="Template Error\n\n";for(var c in a)b+="<"+c+">\n"+a[c]+"\n\n";"object"==typeof console&&console.error(b)};var p=function(a){return d.onerror(a),function(){return"{Template Error}"}},q=d.compile=function(a,b){function d(c){try{return new i(c,h)+""}catch(d){return b.debug?p(d)():(b.debug=!0,q(a,b)(c))}}b=b||{};for(var g in e)void 0===b[g]&&(b[g]=e[g]);var h=b.filename;try{var i=c(a,b)}catch(j){return j.filename=h||"anonymous",j.name="Syntax Error",p(j)}return d.prototype=i.prototype,d.toString=function(){return i.toString()},h&&b.cache&&(f[h]=d),d},r=n.$each,s="break,case,catch,continue,debugger,default,delete,do,else,false,finally,for,function,if,in,instanceof,new,null,return,switch,this,throw,true,try,typeof,var,void,while,with,abstract,boolean,byte,char,class,const,double,enum,export,extends,final,float,goto,implements,import,int,interface,long,native,package,private,protected,public,short,static,super,synchronized,throws,transient,volatile,arguments,let,yield,undefined",t=/\/\*[\w\W]*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|"(?:[^"\\]|\\[\w\W])*"|'(?:[^'\\]|\\[\w\W])*'|\s*\.\s*[$\w\.]+/g,u=/[^\w$]+/g,v=new RegExp(["\\b"+s.replace(/,/g,"\\b|\\b")+"\\b"].join("|"),"g"),w=/^\d[^,]*|,\d[^,]*/g,x=/^,+|,+$/g,y=/^$|,+/;"function"==typeof define?define(function(){return d}):"undefined"!=typeof exports?module.exports=d:this.template=d}(); -------------------------------------------------------------------------------- /dist/template.js: -------------------------------------------------------------------------------- 1 | /*!art-template - Template Engine | http://aui.github.com/artTemplate/*/ 2 | !function(){function a(a){return a.replace(t,"").replace(u,",").replace(v,"").replace(w,"").replace(x,"").split(y)}function b(a){return"'"+a.replace(/('|\\)/g,"\\$1").replace(/\r/g,"\\r").replace(/\n/g,"\\n")+"'"}function c(c,d){function e(a){return m+=a.split(/\n/).length-1,k&&(a=a.replace(/\s+/g," ").replace(//g,"")),a&&(a=s[1]+b(a)+s[2]+"\n"),a}function f(b){var c=m;if(j?b=j(b,d):g&&(b=b.replace(/\n/g,function(){return m++,"$line="+m+";"})),0===b.indexOf("=")){var e=l&&!/^=[=#]/.test(b);if(b=b.replace(/^=[=#]?|[\s;]*$/g,""),e){var f=b.replace(/\s*\([^\)]+\)/,"");n[f]||/^(include|print)$/.test(f)||(b="$escape("+b+")")}else b="$string("+b+")";b=s[1]+b+s[2]}return g&&(b="$line="+c+";"+b),r(a(b),function(a){if(a&&!p[a]){var b;b="print"===a?u:"include"===a?v:n[a]?"$utils."+a:o[a]?"$helpers."+a:"$data."+a,w+=a+"="+b+",",p[a]=!0}}),b+"\n"}var g=d.debug,h=d.openTag,i=d.closeTag,j=d.parser,k=d.compress,l=d.escape,m=1,p={$data:1,$filename:1,$utils:1,$helpers:1,$out:1,$line:1},q="".trim,s=q?["$out='';","$out+=",";","$out"]:["$out=[];","$out.push(",");","$out.join('')"],t=q?"$out+=text;return $out;":"$out.push(text);",u="function(){var text=''.concat.apply('',arguments);"+t+"}",v="function(filename,data){data=data||$data;var text=$utils.$include(filename,data,$filename);"+t+"}",w="'use strict';var $utils=this,$helpers=$utils.$helpers,"+(g?"$line=0,":""),x=s[0],y="return new String("+s[3]+");";r(c.split(h),function(a){a=a.split(i);var b=a[0],c=a[1];1===a.length?x+=e(b):(x+=f(b),c&&(x+=e(c)))});var z=w+x+y;g&&(z="try{"+z+"}catch(e){throw {filename:$filename,name:'Render Error',message:e.message,line:$line,source:"+b(c)+".split(/\\n/)[$line-1].replace(/^\\s+/,'')};}");try{var A=new Function("$data","$filename",z);return A.prototype=n,A}catch(B){throw B.temp="function anonymous($data,$filename) {"+z+"}",B}}var d=function(a,b){return"string"==typeof b?q(b,{filename:a}):g(a,b)};d.version="3.0.0",d.config=function(a,b){e[a]=b};var e=d.defaults={openTag:"<%",closeTag:"%>",escape:!0,cache:!0,compress:!1,parser:null},f=d.cache={};d.render=function(a,b){return q(a,b)};var g=d.renderFile=function(a,b){var c=d.get(a)||p({filename:a,name:"Render Error",message:"Template not found"});return b?c(b):c};d.get=function(a){var b;if(f[a])b=f[a];else if("object"==typeof document){var c=document.getElementById(a);if(c){var d=(c.value||c.innerHTML).replace(/^\s*|\s*$/g,"");b=q(d,{filename:a})}}return b};var h=function(a,b){return"string"!=typeof a&&(b=typeof a,"number"===b?a+="":a="function"===b?h(a.call(a)):""),a},i={"<":"<",">":">",'"':""","'":"'","&":"&"},j=function(a){return i[a]},k=function(a){return h(a).replace(/&(?![\w#]+;)|[<>"']/g,j)},l=Array.isArray||function(a){return"[object Array]"==={}.toString.call(a)},m=function(a,b){var c,d;if(l(a))for(c=0,d=a.length;d>c;c++)b.call(a,a[c],c,a);else for(c in a)b.call(a,a[c],c)},n=d.utils={$helpers:{},$include:g,$string:h,$escape:k,$each:m};d.helper=function(a,b){o[a]=b};var o=d.helpers=n.$helpers;d.onerror=function(a){var b="Template Error\n\n";for(var c in a)b+="<"+c+">\n"+a[c]+"\n\n";"object"==typeof console&&console.error(b)};var p=function(a){return d.onerror(a),function(){return"{Template Error}"}},q=d.compile=function(a,b){function d(c){try{return new i(c,h)+""}catch(d){return b.debug?p(d)():(b.debug=!0,q(a,b)(c))}}b=b||{};for(var g in e)void 0===b[g]&&(b[g]=e[g]);var h=b.filename;try{var i=c(a,b)}catch(j){return j.filename=h||"anonymous",j.name="Syntax Error",p(j)}return d.prototype=i.prototype,d.toString=function(){return i.toString()},h&&b.cache&&(f[h]=d),d},r=n.$each,s="break,case,catch,continue,debugger,default,delete,do,else,false,finally,for,function,if,in,instanceof,new,null,return,switch,this,throw,true,try,typeof,var,void,while,with,abstract,boolean,byte,char,class,const,double,enum,export,extends,final,float,goto,implements,import,int,interface,long,native,package,private,protected,public,short,static,super,synchronized,throws,transient,volatile,arguments,let,yield,undefined",t=/\/\*[\w\W]*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|"(?:[^"\\]|\\[\w\W])*"|'(?:[^'\\]|\\[\w\W])*'|\s*\.\s*[$\w\.]+/g,u=/[^\w$]+/g,v=new RegExp(["\\b"+s.replace(/,/g,"\\b|\\b")+"\\b"].join("|"),"g"),w=/^\d[^,]*|,\d[^,]*/g,x=/^,+|,+$/g,y=/^$|,+/;e.openTag="{{",e.closeTag="}}";var z=function(a,b){var c=b.split(":"),d=c.shift(),e=c.join(":")||"";return e&&(e=", "+e),"$helpers."+d+"("+a+e+")"};e.parser=function(a){a=a.replace(/^\s/,"");var b=a.split(" "),c=b.shift(),e=b.join(" ");switch(c){case"if":a="if("+e+"){";break;case"else":b="if"===b.shift()?" if("+b.join(" ")+")":"",a="}else"+b+"{";break;case"/if":a="}";break;case"each":var f=b[0]||"$data",g=b[1]||"as",h=b[2]||"$value",i=b[3]||"$index",j=h+","+i;"as"!==g&&(f="[]"),a="$each("+f+",function("+j+"){";break;case"/each":a="});";break;case"echo":a="print("+e+");";break;case"print":case"include":a=c+"("+b.join(",")+");";break;default:if(/^\s*\|\s*[\w\$]/.test(e)){var k=!0;0===a.indexOf("#")&&(a=a.substr(1),k=!1);for(var l=0,m=a.split("|"),n=m.length,o=m[l++];n>l;l++)o=z(o,m[l]);a=(k?"=":"=#")+o}else a=d.helpers[c]?"=#"+c+"("+b.join(",")+");":"="+a}return a},"function"==typeof define?define(function(){return d}):"undefined"!=typeof exports?module.exports=d:this.template=d}(); -------------------------------------------------------------------------------- /doc/syntax-native.md: -------------------------------------------------------------------------------- 1 | # artTemplate 原生 js 模板语法版 2 | 3 | ## 使用 4 | 5 | 在页面中引用模板引擎: 6 | 7 | 8 | 9 | [下载](https://raw.github.com/aui/artTemplate/master/dist/template-native.js) 10 | 11 | ## 表达式 12 | 13 | ``<%`` 与 ``%>`` 符号包裹起来的语句则为模板的逻辑表达式。 14 | 15 | ### 输出表达式 16 | 17 | 对内容编码输出: 18 | 19 | <%=content%> 20 | 21 | 不编码输出: 22 | 23 | <%=#content%> 24 | 25 | 编码可以防止数据中含有 HTML 字符串,避免引起 XSS 攻击。 26 | 27 | ### 逻辑 28 | 29 | 支持使用 js 原生语法 30 | 31 |

<%=title%>

32 |
    33 | <%for(i = 0; i < list.length; i ++) {%> 34 |
  • 条目内容 <%=i + 1%> :<%=list[i]%>
  • 35 | <%}%> 36 |
37 | 38 | > 模板不能访问全局对象,公用的方法请参见文档[辅助方法](#辅助方法)章节 39 | 40 | ### 模板包含表达式 41 | 42 | 用于嵌入子模板。 43 | 44 | <% include('template_name') %> 45 | 46 | 子模板默认共享当前数据,亦可以指定数据: 47 | 48 | <% include('template_name', news_list) %> 49 | 50 | ## 辅助方法 51 | 52 | 使用``template.helper(name, callback)``注册公用辅助方法: 53 | 54 | template.helper('dateFormat', function (date, format) { 55 | // .. 56 | return value; 57 | }); 58 | 59 | 模板中使用的方式: 60 | 61 | <%=dateFormat(content) %> 62 | 63 | ## 演示例子 64 | 65 | * [基本例子](http://aui.github.io/artTemplate/demo/template-native/basic.html) 66 | * [不转义HTML](http://aui.github.io/artTemplate/demo/template-native/no-escape.html) 67 | * [在javascript中存放模板](http://aui.github.io/artTemplate/demo/template-native/compile.html) 68 | * [嵌入子模板(include)](http://aui.github.io/artTemplate/demo/template-native/include.html) 69 | * [访问外部公用函数(辅助方法)](http://aui.github.io/artTemplate/demo/template-native/helper.html) 70 | 71 | ---------------------------------------------- 72 | 73 | 本文档针对 artTemplate v3.0.0 编写 -------------------------------------------------------------------------------- /doc/syntax-simple.md: -------------------------------------------------------------------------------- 1 | # artTemplate 简洁语法版 2 | 3 | ## 使用 4 | 5 | 引用简洁语法的引擎版本,例如: 6 | 7 | 8 | 9 | [下载](https://raw.github.com/aui/artTemplate/master/dist/template.js) 10 | 11 | ## 表达式 12 | 13 | ``{{`` 与 ``}}`` 符号包裹起来的语句则为模板的逻辑表达式。 14 | 15 | ### 输出表达式 16 | 17 | 对内容编码输出: 18 | 19 | {{content}} 20 | 21 | 不编码输出: 22 | 23 | {{#content}} 24 | 25 | 编码可以防止数据中含有 HTML 字符串,避免引起 XSS 攻击。 26 | 27 | ### 条件表达式 28 | 29 | {{if admin}} 30 |

admin

31 | {{else if code > 0}} 32 |

master

33 | {{else}} 34 |

error!

35 | {{/if}} 36 | 37 | ### 遍历表达式 38 | 39 | 无论数组或者对象都可以用 each 进行遍历。 40 | 41 | {{each list as value index}} 42 |
  • {{index}} - {{value.user}}
  • 43 | {{/each}} 44 | 45 | 亦可以被简写: 46 | 47 | {{each list}} 48 |
  • {{$index}} - {{$value.user}}
  • 49 | {{/each}} 50 | 51 | ### 模板包含表达式 52 | 53 | 用于嵌入子模板。 54 | 55 | {{include 'template_name'}} 56 | 57 | 子模板默认共享当前数据,亦可以指定数据: 58 | 59 | {{include 'template_name' news_list}} 60 | 61 | ## 辅助方法 62 | 63 | 使用``template.helper(name, callback)``注册公用辅助方法: 64 | 65 | ``` 66 | template.helper('dateFormat', function (date, format) { 67 | // .. 68 | return value; 69 | }); 70 | ``` 71 | 72 | 模板中使用的方式: 73 | 74 | {{time | dateFormat:'yyyy-MM-dd hh:mm:ss'}} 75 | 76 | 支持传入参数与嵌套使用: 77 | 78 | {{time | say:'cd' | ubb | link}} 79 | 80 | ## 演示例子 81 | 82 | * [基本例子](http://aui.github.io/artTemplate/demo/basic.html) 83 | * [不转义HTML](http://aui.github.io/artTemplate/demo/no-escape.html) 84 | * [在javascript中存放模板](http://aui.github.io/artTemplate/demo/compile.html) 85 | * [嵌入子模板(include)](http://aui.github.io/artTemplate/demo/include.html) 86 | * [访问外部公用函数(辅助方法)](http://aui.github.io/artTemplate/demo/helper.html) 87 | 88 | ---------------------------------------------- 89 | 90 | 本文档针对 artTemplate v3.0.0+ 编写 -------------------------------------------------------------------------------- /node/_node.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | module.exports = function (template) { 5 | 6 | var cacheStore = template.cache; 7 | var defaults = template.defaults; 8 | var rExtname; 9 | 10 | // 提供新的配置字段 11 | defaults.base = ''; 12 | defaults.extname = '.html'; 13 | defaults.encoding = 'utf-8'; 14 | 15 | function compileFromFS(filename) { 16 | // 加载模板并编译 17 | var source = readTemplate(filename); 18 | 19 | if (typeof source === 'string') { 20 | return template.compile(source, { 21 | filename: filename 22 | }); 23 | } 24 | } 25 | 26 | // 重写引擎编译结果获取方法 27 | template.get = function (filename) { 28 | 29 | var fn; 30 | 31 | 32 | if (cacheStore.hasOwnProperty(filename)) { 33 | // 使用内存缓存 34 | fn = cacheStore[filename]; 35 | } else { 36 | fn = compileFromFS(filename); 37 | 38 | if (fn) { 39 | var watcher = fs.watch(filename + defaults.extname); 40 | 41 | // 文件发生改变,重新生成缓存 42 | // TODO: 观察删除文件,或者其他使文件发生变化的改动 43 | watcher.on('change', function (event) { 44 | if (event === 'change') { 45 | cacheStore[filename] = compileFromFS(filename); 46 | } 47 | }); 48 | } 49 | } 50 | 51 | return fn; 52 | }; 53 | 54 | 55 | function readTemplate (id) { 56 | id = path.join(defaults.base, id + defaults.extname); 57 | 58 | if (id.indexOf(defaults.base) !== 0) { 59 | // 安全限制:禁止超出模板目录之外调用文件 60 | throw new Error('"' + id + '" is not in the template directory'); 61 | } else { 62 | try { 63 | return fs.readFileSync(id, defaults.encoding); 64 | } catch (e) {} 65 | } 66 | } 67 | 68 | 69 | // 重写模板`include``语句实现方法,转换模板为绝对路径 70 | template.utils.$include = function (filename, data, from) { 71 | 72 | from = path.dirname(from); 73 | filename = path.join(from, filename); 74 | 75 | return template.renderFile(filename, data); 76 | } 77 | 78 | 79 | // express support 80 | template.__express = function (file, options, fn) { 81 | 82 | if (typeof options === 'function') { 83 | fn = options; 84 | options = {}; 85 | } 86 | 87 | 88 | if (!rExtname) { 89 | // 去掉 express 传入的路径 90 | rExtname = new RegExp((defaults.extname + '$').replace(/\./g, '\\.')); 91 | } 92 | 93 | 94 | file = file.replace(rExtname, ''); 95 | 96 | options.filename = file; 97 | fn(null, template.renderFile(file, options)); 98 | }; 99 | 100 | 101 | return template; 102 | } -------------------------------------------------------------------------------- /node/template-native.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * artTemplate[NodeJS] 3 | * https://github.com/aui/artTemplate 4 | * Released under the MIT, BSD, and GPL Licenses 5 | */ 6 | 7 | var node = require('./_node.js'); 8 | var template = require('../dist/template-native-debug.js'); 9 | module.exports = node(template); -------------------------------------------------------------------------------- /node/template.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * artTemplate[NodeJS] 3 | * https://github.com/aui/artTemplate 4 | * Released under the MIT, BSD, and GPL Licenses 5 | */ 6 | 7 | var node = require('./_node.js'); 8 | var template = require('../dist/template-debug.js'); 9 | module.exports = node(template); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "art-template", 3 | "description": "JavaScript Template Engine", 4 | "homepage": "http://aui.github.com/artTemplate/", 5 | "keywords": [ 6 | "util", 7 | "functional", 8 | "template" 9 | ], 10 | "author": "tangbin ", 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/aui/artTemplate.git" 14 | }, 15 | "main": "./node/template.js", 16 | "version": "3.0.3", 17 | "devDependencies": { 18 | "grunt-cli": "*", 19 | "grunt": "*", 20 | "grunt-contrib-jshint": "*", 21 | "grunt-contrib-concat": "*", 22 | "grunt-contrib-uglify": "*" 23 | }, 24 | "license": "BSD" 25 | } 26 | -------------------------------------------------------------------------------- /src/cache.js: -------------------------------------------------------------------------------- 1 | var cacheStore = template.cache = {}; 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/compile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 编译模板 3 | * 2012-6-6 @TooBug: define 方法名改为 compile,与 Node Express 保持一致 4 | * @name template.compile 5 | * @param {String} 模板字符串 6 | * @param {Object} 编译选项 7 | * 8 | * - openTag {String} 9 | * - closeTag {String} 10 | * - filename {String} 11 | * - escape {Boolean} 12 | * - compress {Boolean} 13 | * - debug {Boolean} 14 | * - cache {Boolean} 15 | * - parser {Function} 16 | * 17 | * @return {Function} 渲染方法 18 | */ 19 | var compile = template.compile = function (source, options) { 20 | 21 | // 合并默认配置 22 | options = options || {}; 23 | for (var name in defaults) { 24 | if (options[name] === undefined) { 25 | options[name] = defaults[name]; 26 | } 27 | } 28 | 29 | 30 | var filename = options.filename; 31 | 32 | 33 | try { 34 | 35 | var Render = compiler(source, options); 36 | 37 | } catch (e) { 38 | 39 | e.filename = filename || 'anonymous'; 40 | e.name = 'Syntax Error'; 41 | 42 | return showDebugInfo(e); 43 | 44 | } 45 | 46 | 47 | // 对编译结果进行一次包装 48 | 49 | function render (data) { 50 | 51 | try { 52 | 53 | return new Render(data, filename) + ''; 54 | 55 | } catch (e) { 56 | 57 | // 运行时出错后自动开启调试模式重新编译 58 | if (!options.debug) { 59 | options.debug = true; 60 | return compile(source, options)(data); 61 | } 62 | 63 | return showDebugInfo(e)(); 64 | 65 | } 66 | 67 | } 68 | 69 | 70 | render.prototype = Render.prototype; 71 | render.toString = function () { 72 | return Render.toString(); 73 | }; 74 | 75 | 76 | if (filename && options.cache) { 77 | cacheStore[filename] = render; 78 | } 79 | 80 | 81 | return render; 82 | 83 | }; 84 | 85 | 86 | 87 | 88 | // 数组迭代 89 | var forEach = utils.$each; 90 | 91 | 92 | // 静态分析模板变量 93 | var KEYWORDS = 94 | // 关键字 95 | 'break,case,catch,continue,debugger,default,delete,do,else,false' 96 | + ',finally,for,function,if,in,instanceof,new,null,return,switch,this' 97 | + ',throw,true,try,typeof,var,void,while,with' 98 | 99 | // 保留字 100 | + ',abstract,boolean,byte,char,class,const,double,enum,export,extends' 101 | + ',final,float,goto,implements,import,int,interface,long,native' 102 | + ',package,private,protected,public,short,static,super,synchronized' 103 | + ',throws,transient,volatile' 104 | 105 | // ECMA 5 - use strict 106 | + ',arguments,let,yield' 107 | 108 | + ',undefined'; 109 | 110 | var REMOVE_RE = /\/\*[\w\W]*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|"(?:[^"\\]|\\[\w\W])*"|'(?:[^'\\]|\\[\w\W])*'|\s*\.\s*[$\w\.]+/g; 111 | var SPLIT_RE = /[^\w$]+/g; 112 | var KEYWORDS_RE = new RegExp(["\\b" + KEYWORDS.replace(/,/g, '\\b|\\b') + "\\b"].join('|'), 'g'); 113 | var NUMBER_RE = /^\d[^,]*|,\d[^,]*/g; 114 | var BOUNDARY_RE = /^,+|,+$/g; 115 | var SPLIT2_RE = /^$|,+/; 116 | 117 | 118 | // 获取变量 119 | function getVariable (code) { 120 | return code 121 | .replace(REMOVE_RE, '') 122 | .replace(SPLIT_RE, ',') 123 | .replace(KEYWORDS_RE, '') 124 | .replace(NUMBER_RE, '') 125 | .replace(BOUNDARY_RE, '') 126 | .split(SPLIT2_RE); 127 | }; 128 | 129 | 130 | // 字符串转义 131 | function stringify (code) { 132 | return "'" + code 133 | // 单引号与反斜杠转义 134 | .replace(/('|\\)/g, '\\$1') 135 | // 换行符转义(windows + linux) 136 | .replace(/\r/g, '\\r') 137 | .replace(/\n/g, '\\n') + "'"; 138 | } 139 | 140 | 141 | function compiler (source, options) { 142 | 143 | var debug = options.debug; 144 | var openTag = options.openTag; 145 | var closeTag = options.closeTag; 146 | var parser = options.parser; 147 | var compress = options.compress; 148 | var escape = options.escape; 149 | 150 | 151 | 152 | var line = 1; 153 | var uniq = {$data:1,$filename:1,$utils:1,$helpers:1,$out:1,$line:1}; 154 | 155 | 156 | 157 | var isNewEngine = ''.trim;// '__proto__' in {} 158 | var replaces = isNewEngine 159 | ? ["$out='';", "$out+=", ";", "$out"] 160 | : ["$out=[];", "$out.push(", ");", "$out.join('')"]; 161 | 162 | var concat = isNewEngine 163 | ? "$out+=text;return $out;" 164 | : "$out.push(text);"; 165 | 166 | var print = "function(){" 167 | + "var text=''.concat.apply('',arguments);" 168 | + concat 169 | + "}"; 170 | 171 | var include = "function(filename,data){" 172 | + "data=data||$data;" 173 | + "var text=$utils.$include(filename,data,$filename);" 174 | + concat 175 | + "}"; 176 | 177 | var headerCode = "'use strict';" 178 | + "var $utils=this,$helpers=$utils.$helpers," 179 | + (debug ? "$line=0," : ""); 180 | 181 | var mainCode = replaces[0]; 182 | 183 | var footerCode = "return new String(" + replaces[3] + ");" 184 | 185 | // html与逻辑语法分离 186 | forEach(source.split(openTag), function (code) { 187 | code = code.split(closeTag); 188 | 189 | var $0 = code[0]; 190 | var $1 = code[1]; 191 | 192 | // code: [html] 193 | if (code.length === 1) { 194 | 195 | mainCode += html($0); 196 | 197 | // code: [logic, html] 198 | } else { 199 | 200 | mainCode += logic($0); 201 | 202 | if ($1) { 203 | mainCode += html($1); 204 | } 205 | } 206 | 207 | 208 | }); 209 | 210 | var code = headerCode + mainCode + footerCode; 211 | 212 | // 调试语句 213 | if (debug) { 214 | code = "try{" + code + "}catch(e){" 215 | + "throw {" 216 | + "filename:$filename," 217 | + "name:'Render Error'," 218 | + "message:e.message," 219 | + "line:$line," 220 | + "source:" + stringify(source) 221 | + ".split(/\\n/)[$line-1].replace(/^\\s+/,'')" 222 | + "};" 223 | + "}"; 224 | } 225 | 226 | 227 | 228 | try { 229 | 230 | 231 | var Render = new Function("$data", "$filename", code); 232 | Render.prototype = utils; 233 | 234 | return Render; 235 | 236 | } catch (e) { 237 | e.temp = "function anonymous($data,$filename) {" + code + "}"; 238 | throw e; 239 | } 240 | 241 | 242 | 243 | 244 | // 处理 HTML 语句 245 | function html (code) { 246 | 247 | // 记录行号 248 | line += code.split(/\n/).length - 1; 249 | 250 | // 压缩多余空白与注释 251 | if (compress) { 252 | code = code 253 | .replace(/\s+/g, ' ') 254 | .replace(//g, ''); 255 | } 256 | 257 | if (code) { 258 | code = replaces[1] + stringify(code) + replaces[2] + "\n"; 259 | } 260 | 261 | return code; 262 | } 263 | 264 | 265 | // 处理逻辑语句 266 | function logic (code) { 267 | 268 | var thisLine = line; 269 | 270 | if (parser) { 271 | 272 | // 语法转换插件钩子 273 | code = parser(code, options); 274 | 275 | } else if (debug) { 276 | 277 | // 记录行号 278 | code = code.replace(/\n/g, function () { 279 | line ++; 280 | return "$line=" + line + ";"; 281 | }); 282 | 283 | } 284 | 285 | 286 | // 输出语句. 编码: <%=value%> 不编码:<%=#value%> 287 | // <%=#value%> 等同 v2.0.3 之前的 <%==value%> 288 | if (code.indexOf('=') === 0) { 289 | 290 | var escapeSyntax = escape && !/^=[=#]/.test(code); 291 | 292 | code = code.replace(/^=[=#]?|[\s;]*$/g, ''); 293 | 294 | // 对内容编码 295 | if (escapeSyntax) { 296 | 297 | var name = code.replace(/\s*\([^\)]+\)/, ''); 298 | 299 | // 排除 utils.* | include | print 300 | 301 | if (!utils[name] && !/^(include|print)$/.test(name)) { 302 | code = "$escape(" + code + ")"; 303 | } 304 | 305 | // 不编码 306 | } else { 307 | code = "$string(" + code + ")"; 308 | } 309 | 310 | 311 | code = replaces[1] + code + replaces[2]; 312 | 313 | } 314 | 315 | if (debug) { 316 | code = "$line=" + thisLine + ";" + code; 317 | } 318 | 319 | // 提取模板中的变量名 320 | forEach(getVariable(code), function (name) { 321 | 322 | // name 值可能为空,在安卓低版本浏览器下 323 | if (!name || uniq[name]) { 324 | return; 325 | } 326 | 327 | var value; 328 | 329 | // 声明模板变量 330 | // 赋值优先级: 331 | // [include, print] > utils > helpers > data 332 | if (name === 'print') { 333 | 334 | value = print; 335 | 336 | } else if (name === 'include') { 337 | 338 | value = include; 339 | 340 | } else if (utils[name]) { 341 | 342 | value = "$utils." + name; 343 | 344 | } else if (helpers[name]) { 345 | 346 | value = "$helpers." + name; 347 | 348 | } else { 349 | 350 | value = "$data." + name; 351 | } 352 | 353 | headerCode += name + "=" + value + ","; 354 | uniq[name] = true; 355 | 356 | 357 | }); 358 | 359 | return code + "\n"; 360 | } 361 | 362 | 363 | }; 364 | 365 | 366 | 367 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 设置全局配置 3 | * @name template.config 4 | * @param {String} 名称 5 | * @param {Any} 值 6 | */ 7 | template.config = function (name, value) { 8 | defaults[name] = value; 9 | }; 10 | 11 | 12 | 13 | var defaults = template.defaults = { 14 | openTag: '<%', // 逻辑语法开始标签 15 | closeTag: '%>', // 逻辑语法结束标签 16 | escape: true, // 是否编码输出变量的 HTML 字符 17 | cache: true, // 是否开启缓存(依赖 options 的 filename 字段) 18 | compress: false, // 是否压缩输出 19 | parser: null // 自定义语法格式器 @see: template-syntax.js 20 | }; 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/get.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取编译缓存(可由外部重写此方法) 3 | * @param {String} 模板名 4 | * @param {Function} 编译好的函数 5 | */ 6 | template.get = function (filename) { 7 | 8 | var cache; 9 | 10 | if (cacheStore[filename]) { 11 | // 使用内存缓存 12 | cache = cacheStore[filename]; 13 | } else if (typeof document === 'object') { 14 | // 加载模板并编译 15 | var elem = document.getElementById(filename); 16 | 17 | if (elem) { 18 | var source = (elem.value || elem.innerHTML) 19 | .replace(/^\s*|\s*$/g, ''); 20 | cache = compile(source, { 21 | filename: filename 22 | }); 23 | } 24 | } 25 | 26 | return cache; 27 | }; 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 添加模板辅助方法 3 | * @name template.helper 4 | * @param {String} 名称 5 | * @param {Function} 方法 6 | */ 7 | template.helper = function (name, helper) { 8 | helpers[name] = helper; 9 | }; 10 | 11 | var helpers = template.helpers = utils.$helpers; 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/intro.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * artTemplate - Template Engine 3 | * https://github.com/aui/artTemplate 4 | * Released under the MIT, BSD, and GPL Licenses 5 | */ 6 | 7 | !(function () { 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/onerror.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 模板错误事件(可由外部重写此方法) 3 | * @name template.onerror 4 | * @event 5 | */ 6 | template.onerror = function (e) { 7 | var message = 'Template Error\n\n'; 8 | for (var name in e) { 9 | message += '<' + name + '>\n' + e[name] + '\n\n'; 10 | } 11 | 12 | if (typeof console === 'object') { 13 | console.error(message); 14 | } 15 | }; 16 | 17 | 18 | // 模板调试器 19 | var showDebugInfo = function (e) { 20 | 21 | template.onerror(e); 22 | 23 | return function () { 24 | return '{Template Error}'; 25 | }; 26 | }; 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/outro.js: -------------------------------------------------------------------------------- 1 | 2 | // RequireJS && SeaJS 3 | if (typeof define === 'function') { 4 | define(function() { 5 | return template; 6 | }); 7 | 8 | // NodeJS 9 | } else if (typeof exports !== 'undefined') { 10 | module.exports = template; 11 | } else { 12 | this.template = template; 13 | } 14 | 15 | })(); -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 渲染模板 3 | * @name template.render 4 | * @param {String} 模板 5 | * @param {Object} 数据 6 | * @return {String} 渲染好的字符串 7 | */ 8 | template.render = function (source, options) { 9 | return compile(source, options); 10 | }; 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/renderFile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 渲染模板(根据模板名) 3 | * @name template.render 4 | * @param {String} 模板名 5 | * @param {Object} 数据 6 | * @return {String} 渲染好的字符串 7 | */ 8 | var renderFile = template.renderFile = function (filename, data) { 9 | var fn = template.get(filename) || showDebugInfo({ 10 | filename: filename, 11 | name: 'Render Error', 12 | message: 'Template not found' 13 | }); 14 | return data ? fn(data) : fn; 15 | }; 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/syntax.js: -------------------------------------------------------------------------------- 1 | // 定义模板引擎的语法 2 | 3 | 4 | defaults.openTag = '{{'; 5 | defaults.closeTag = '}}'; 6 | 7 | 8 | var filtered = function (js, filter) { 9 | var parts = filter.split(':'); 10 | var name = parts.shift(); 11 | var args = parts.join(':') || ''; 12 | 13 | if (args) { 14 | args = ', ' + args; 15 | } 16 | 17 | return '$helpers.' + name + '(' + js + args + ')'; 18 | } 19 | 20 | 21 | defaults.parser = function (code, options) { 22 | 23 | // var match = code.match(/([\w\$]*)(\b.*)/); 24 | // var key = match[1]; 25 | // var args = match[2]; 26 | // var split = args.split(' '); 27 | // split.shift(); 28 | 29 | code = code.replace(/^\s/, ''); 30 | 31 | var split = code.split(' '); 32 | var key = split.shift(); 33 | var args = split.join(' '); 34 | 35 | 36 | 37 | switch (key) { 38 | 39 | case 'if': 40 | 41 | code = 'if(' + args + '){'; 42 | break; 43 | 44 | case 'else': 45 | 46 | if (split.shift() === 'if') { 47 | split = ' if(' + split.join(' ') + ')'; 48 | } else { 49 | split = ''; 50 | } 51 | 52 | code = '}else' + split + '{'; 53 | break; 54 | 55 | case '/if': 56 | 57 | code = '}'; 58 | break; 59 | 60 | case 'each': 61 | 62 | var object = split[0] || '$data'; 63 | var as = split[1] || 'as'; 64 | var value = split[2] || '$value'; 65 | var index = split[3] || '$index'; 66 | 67 | var param = value + ',' + index; 68 | 69 | if (as !== 'as') { 70 | object = '[]'; 71 | } 72 | 73 | code = '$each(' + object + ',function(' + param + '){'; 74 | break; 75 | 76 | case '/each': 77 | 78 | code = '});'; 79 | break; 80 | 81 | case 'echo': 82 | 83 | code = 'print(' + args + ');'; 84 | break; 85 | 86 | case 'print': 87 | case 'include': 88 | 89 | code = key + '(' + split.join(',') + ');'; 90 | break; 91 | 92 | default: 93 | 94 | // 过滤器(辅助方法) 95 | // {{value | filterA:'abcd' | filterB}} 96 | // >>> $helpers.filterB($helpers.filterA(value, 'abcd')) 97 | // TODO: {{ddd||aaa}} 不包含空格 98 | if (/^\s*\|\s*[\w\$]/.test(args)) { 99 | 100 | var escape = true; 101 | 102 | // {{#value | link}} 103 | if (code.indexOf('#') === 0) { 104 | code = code.substr(1); 105 | escape = false; 106 | } 107 | 108 | var i = 0; 109 | var array = code.split('|'); 110 | var len = array.length; 111 | var val = array[i++]; 112 | 113 | for (; i < len; i ++) { 114 | val = filtered(val, array[i]); 115 | } 116 | 117 | code = (escape ? '=' : '=#') + val; 118 | 119 | // 即将弃用 {{helperName value}} 120 | } else if (template.helpers[key]) { 121 | 122 | code = '=#' + key + '(' + split.join(',') + ');'; 123 | 124 | // 内容直接输出 {{value}} 125 | } else { 126 | 127 | code = '=' + code; 128 | } 129 | 130 | break; 131 | } 132 | 133 | 134 | return code; 135 | }; 136 | 137 | 138 | -------------------------------------------------------------------------------- /src/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 模板引擎 3 | * @name template 4 | * @param {String} 模板名 5 | * @param {Object, String} 数据。如果为字符串则编译并缓存编译结果 6 | * @return {String, Function} 渲染好的HTML字符串或者渲染方法 7 | */ 8 | var template = function (filename, content) { 9 | return typeof content === 'string' 10 | ? compile(content, { 11 | filename: filename 12 | }) 13 | : renderFile(filename, content); 14 | }; 15 | 16 | 17 | template.version = '3.0.0'; 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | var toString = function (value, type) { 2 | 3 | if (typeof value !== 'string') { 4 | 5 | type = typeof value; 6 | if (type === 'number') { 7 | value += ''; 8 | } else if (type === 'function') { 9 | value = toString(value.call(value)); 10 | } else { 11 | value = ''; 12 | } 13 | } 14 | 15 | return value; 16 | 17 | }; 18 | 19 | 20 | var escapeMap = { 21 | "<": "<", 22 | ">": ">", 23 | '"': """, 24 | "'": "'", 25 | "&": "&" 26 | }; 27 | 28 | 29 | var escapeFn = function (s) { 30 | return escapeMap[s]; 31 | }; 32 | 33 | var escapeHTML = function (content) { 34 | return toString(content) 35 | .replace(/&(?![\w#]+;)|[<>"']/g, escapeFn); 36 | }; 37 | 38 | 39 | var isArray = Array.isArray || function (obj) { 40 | return ({}).toString.call(obj) === '[object Array]'; 41 | }; 42 | 43 | 44 | var each = function (data, callback) { 45 | var i, len; 46 | if (isArray(data)) { 47 | for (i = 0, len = data.length; i < len; i++) { 48 | callback.call(data, data[i], i, data); 49 | } 50 | } else { 51 | for (i in data) { 52 | callback.call(data, data[i], i); 53 | } 54 | } 55 | }; 56 | 57 | 58 | var utils = template.utils = { 59 | 60 | $helpers: {}, 61 | 62 | $include: renderFile, 63 | 64 | $string: toString, 65 | 66 | $escape: escapeHTML, 67 | 68 | $each: each 69 | 70 | }; -------------------------------------------------------------------------------- /test/js/baiduTemplate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * baiduTemplate简单好用的Javascript模板引擎 1.0.6 版本 3 | * http://baidufe.github.com/BaiduTemplate 4 | * 开源协议:BSD License 5 | * 浏览器环境占用命名空间 baidu.template ,nodejs环境直接安装 npm install baidutemplate 6 | * @param str{String} dom结点ID,或者模板string 7 | * @param data{Object} 需要渲染的json对象,可以为空。当data为{}时,仍然返回html。 8 | * @return 如果无data,直接返回编译后的函数;如果有data,返回html。 9 | * @author wangxiao 10 | * @email 1988wangxiao@gmail.com 11 | */ 12 | 13 | ;(function(window){ 14 | 15 | //取得浏览器环境的baidu命名空间,非浏览器环境符合commonjs规范exports出去 16 | //修正在nodejs环境下,采用baidu.template变量名 17 | var baidu = typeof module === 'undefined' ? (window.baidu = window.baidu || {}) : module.exports; 18 | 19 | //模板函数(放置于baidu.template命名空间下) 20 | baidu.template = function(str, data){ 21 | 22 | //检查是否有该id的元素存在,如果有元素则获取元素的innerHTML/value,否则认为字符串为模板 23 | var fn = (function(){ 24 | 25 | //判断如果没有document,则为非浏览器环境 26 | if(!window.document){ 27 | return bt._compile(str); 28 | }; 29 | 30 | //HTML5规定ID可以由任何不包含空格字符的字符串组成 31 | var element = document.getElementById(str); 32 | if (element) { 33 | 34 | //取到对应id的dom,缓存其编译后的HTML模板函数 35 | if (bt.cache[str]) { 36 | return bt.cache[str]; 37 | }; 38 | 39 | //textarea或input则取value,其它情况取innerHTML 40 | var html = /^(textarea|input)$/i.test(element.nodeName) ? element.value : element.innerHTML; 41 | return bt._compile(html); 42 | 43 | }else{ 44 | 45 | //是模板字符串,则生成一个函数 46 | //如果直接传入字符串作为模板,则可能变化过多,因此不考虑缓存 47 | return bt._compile(str); 48 | }; 49 | 50 | })(); 51 | 52 | //有数据则返回HTML字符串,没有数据则返回函数 支持data={}的情况 53 | var result = bt._isObject(data) ? fn( data ) : fn; 54 | fn = null; 55 | 56 | return result; 57 | }; 58 | 59 | //取得命名空间 baidu.template 60 | var bt = baidu.template; 61 | 62 | //标记当前版本 63 | bt.versions = bt.versions || []; 64 | bt.versions.push('1.0.6'); 65 | 66 | //缓存 将对应id模板生成的函数缓存下来。 67 | bt.cache = {}; 68 | 69 | //自定义分隔符,可以含有正则中的字符,可以是HTML注释开头 70 | bt.LEFT_DELIMITER = bt.LEFT_DELIMITER||'<%'; 71 | bt.RIGHT_DELIMITER = bt.RIGHT_DELIMITER||'%>'; 72 | 73 | //自定义默认是否转义,默认为默认自动转义 74 | bt.ESCAPE = true; 75 | 76 | //HTML转义 77 | bt._encodeHTML = function (source) { 78 | return String(source) 79 | .replace(/&/g,'&') 80 | .replace(//g,'>') 82 | .replace(/\\/g,'\') 83 | .replace(/"/g,'"') 84 | .replace(/'/g,'''); 85 | }; 86 | 87 | //转义影响正则的字符 88 | bt._encodeReg = function (source) { 89 | return String(source).replace(/([.*+?^=!:${}()|[\]/\\])/g,'\\$1'); 90 | }; 91 | 92 | //转义UI UI变量使用在HTML页面标签onclick等事件函数参数中 93 | bt._encodeEventHTML = function (source) { 94 | return String(source) 95 | .replace(/&/g,'&') 96 | .replace(//g,'>') 98 | .replace(/"/g,'"') 99 | .replace(/'/g,''') 100 | .replace(/\\\\/g,'\\') 101 | .replace(/\\\//g,'\/') 102 | .replace(/\\n/g,'\n') 103 | .replace(/\\r/g,'\r'); 104 | }; 105 | 106 | //将字符串拼接生成函数,即编译过程(compile) 107 | bt._compile = function(str){ 108 | var funBody = "var _template_fun_array=[];\nvar fn=(function(__data__){\nvar _template_varName='';\nfor(name in __data__){\n_template_varName+=('var '+name+'=__data__[\"'+name+'\"];');\n};\neval(_template_varName);\n_template_fun_array.push('"+bt._analysisStr(str)+"');\n_template_varName=null;\n})(_template_object);\nfn = null;\nreturn _template_fun_array.join('');\n"; 109 | return new Function("_template_object",funBody); 110 | }; 111 | 112 | //判断是否是Object类型 113 | bt._isObject = function (source) { 114 | return 'function' === typeof source || !!(source && 'object' === typeof source); 115 | }; 116 | 117 | //解析模板字符串 118 | bt._analysisStr = function(str){ 119 | 120 | //取得分隔符 121 | var _left_ = bt.LEFT_DELIMITER; 122 | var _right_ = bt.RIGHT_DELIMITER; 123 | 124 | //对分隔符进行转义,支持正则中的元字符,可以是HTML注释 125 | var _left = bt._encodeReg(_left_); 126 | var _right = bt._encodeReg(_right_); 127 | 128 | str = String(str) 129 | 130 | //去掉分隔符中js注释 131 | .replace(new RegExp("("+_left+"[^"+_right+"]*)//.*\n","g"), "$1") 132 | 133 | //去掉注释内容 <%* 这里可以任意的注释 *%> 134 | //默认支持HTML注释,将HTML注释匹配掉的原因是用户有可能用 来做分割符 135 | .replace(new RegExp("", "g"),"") 136 | .replace(new RegExp(_left+"\\*.*?\\*"+_right, "g"),"") 137 | 138 | //把所有换行去掉 \r回车符 \t制表符 \n换行符 139 | .replace(new RegExp("[\\r\\t\\n]","g"), "") 140 | 141 | //用来处理非分隔符内部的内容中含有 斜杠 \ 单引号 ‘ ,处理办法为HTML转义 142 | .replace(new RegExp(_left+"(?:(?!"+_right+")[\\s\\S])*"+_right+"|((?:(?!"+_left+")[\\s\\S])+)","g"),function (item, $1) { 143 | var str = ''; 144 | if($1){ 145 | 146 | //将 斜杠 单引 HTML转义 147 | str = $1.replace(/\\/g,"\").replace(/'/g,'''); 148 | while(/<[^<]*?'[^<]*?>/g.test(str)){ 149 | 150 | //将标签内的单引号转义为\r 结合最后一步,替换为\' 151 | str = str.replace(/(<[^<]*?)'([^<]*?>)/g,'$1\r$2') 152 | }; 153 | }else{ 154 | str = item; 155 | } 156 | return str ; 157 | }); 158 | 159 | 160 | str = str 161 | //定义变量,如果没有分号,需要容错 <%var val='test'%> 162 | .replace(new RegExp("("+_left+"[\\s]*?var[\\s]*?.*?[\\s]*?[^;])[\\s]*?"+_right,"g"),"$1;"+_right_) 163 | 164 | //对变量后面的分号做容错(包括转义模式 如<%:h=value%>) <%=value;%> 排除掉函数的情况 <%fun1();%> 排除定义变量情况 <%var val='test';%> 165 | .replace(new RegExp("("+_left+":?[hvu]?[\\s]*?=[\\s]*?[^;|"+_right+"]*?);[\\s]*?"+_right,"g"),"$1"+_right_) 166 | 167 | //按照 <% 分割为一个个数组,再用 \t 和在一起,相当于将 <% 替换为 \t 168 | //将模板按照<%分为一段一段的,再在每段的结尾加入 \t,即用 \t 将每个模板片段前面分隔开 169 | .split(_left_).join("\t"); 170 | 171 | //支持用户配置默认是否自动转义 172 | if(bt.ESCAPE){ 173 | str = str 174 | 175 | //找到 \t=任意一个字符%> 替换为 ‘,任意字符,' 176 | //即替换简单变量 \t=data%> 替换为 ',data,' 177 | //默认HTML转义 也支持HTML转义写法<%:h=value%> 178 | .replace(new RegExp("\\t=(.*?)"+_right,"g"),"',typeof($1) === 'undefined'?'':baidu.template._encodeHTML($1),'"); 179 | }else{ 180 | str = str 181 | 182 | //默认不转义HTML转义 183 | .replace(new RegExp("\\t=(.*?)"+_right,"g"),"',typeof($1) === 'undefined'?'':$1,'"); 184 | }; 185 | 186 | str = str 187 | 188 | //支持HTML转义写法<%:h=value%> 189 | .replace(new RegExp("\\t:h=(.*?)"+_right,"g"),"',typeof($1) === 'undefined'?'':baidu.template._encodeHTML($1),'") 190 | 191 | //支持不转义写法 <%:=value%>和<%-value%> 192 | .replace(new RegExp("\\t(?::=|-)(.*?)"+_right,"g"),"',typeof($1)==='undefined'?'':$1,'") 193 | 194 | //支持url转义 <%:u=value%> 195 | .replace(new RegExp("\\t:u=(.*?)"+_right,"g"),"',typeof($1)==='undefined'?'':encodeURIComponent($1),'") 196 | 197 | //支持UI 变量使用在HTML页面标签onclick等事件函数参数中 <%:v=value%> 198 | .replace(new RegExp("\\t:v=(.*?)"+_right,"g"),"',typeof($1)==='undefined'?'':baidu.template._encodeEventHTML($1),'") 199 | 200 | //将字符串按照 \t 分成为数组,在用'); 将其合并,即替换掉结尾的 \t 为 '); 201 | //在if,for等语句前面加上 '); ,形成 ');if ');for 的形式 202 | .split("\t").join("');") 203 | 204 | //将 %> 替换为_template_fun_array.push(' 205 | //即去掉结尾符,生成函数中的push方法 206 | //如:if(list.length=5){%>

    ',list[4],'

    ');} 207 | //会被替换为 if(list.length=5){_template_fun_array.push('

    ',list[4],'

    ');} 208 | .split(_right_).join("_template_fun_array.push('") 209 | 210 | //将 \r 替换为 \ 211 | .split("\r").join("\\'"); 212 | 213 | return str; 214 | }; 215 | 216 | })(window); 217 | -------------------------------------------------------------------------------- /test/js/doT.js: -------------------------------------------------------------------------------- 1 | // doT.js 2 | // 2011, Laura Doktorova 3 | // https://github.com/olado/doT 4 | // 5 | // doT.js is an open source component of http://bebedo.com 6 | // 7 | // doT is a custom blend of templating functions from jQote2.js 8 | // (jQuery plugin) by aefxx (http://aefxx.com/jquery-plugins/jqote2/) 9 | // and underscore.js (http://documentcloud.github.com/underscore/) 10 | // plus extensions. 11 | // 12 | // Licensed under the MIT license. 13 | // 14 | (function() { 15 | var doT = { version : '0.1.7' }; 16 | 17 | if (typeof module !== 'undefined' && module.exports) { 18 | module.exports = doT; 19 | } else { 20 | this.doT = doT; 21 | } 22 | 23 | doT.templateSettings = { 24 | evaluate: /\{\{([\s\S]+?)\}\}/g, 25 | interpolate: /\{\{=([\s\S]+?)\}\}/g, 26 | encode: /\{\{!([\s\S]+?)\}\}/g, 27 | use: /\{\{#([\s\S]+?)\}\}/g, //compile time evaluation 28 | define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g, //compile time defs 29 | conditionalStart: /\{\{\?([\s\S]+?)\}\}/g, 30 | conditionalEnd: /\{\{\?\}\}/g, 31 | varname: 'it', 32 | strip : true, 33 | append: true 34 | }; 35 | 36 | function resolveDefs(c, block, def) { 37 | return ((typeof block === 'string') ? block : block.toString()) 38 | .replace(c.define, function (match, code, assign, value) { 39 | if (code.indexOf('def.') === 0) { 40 | code = code.substring(4); 41 | } 42 | if (!(code in def)) { 43 | if (assign === ':') { 44 | def[code]= value; 45 | } else { 46 | eval("def[code]=" + value); 47 | } 48 | } 49 | return ''; 50 | }) 51 | .replace(c.use, function(match, code) { 52 | var v = eval(code); 53 | return v ? resolveDefs(c, v, def) : v; 54 | }); 55 | } 56 | 57 | doT.template = function(tmpl, c, def) { 58 | c = c || doT.templateSettings; 59 | var cstart = c.append ? "'+(" : "';out+=(", // optimal choice depends on platform/size of templates 60 | cend = c.append ? ")+'" : ");out+='"; 61 | var str = (c.use || c.define) ? resolveDefs(c, tmpl, def || {}) : tmpl; 62 | 63 | str = ("var out='" + 64 | ((c.strip) ? str.replace(/\s*\s*|[\r\n\t]|(\/\*[\s\S]*?\*\/)/g, ''): str) 65 | .replace(/\\/g, '\\\\') 66 | .replace(/'/g, "\\'") 67 | .replace(c.interpolate, function(match, code) { 68 | return cstart + code.replace(/\\'/g, "'").replace(/\\\\/g,"\\").replace(/[\r\t\n]/g, ' ') + cend; 69 | }) 70 | .replace(c.encode, function(match, code) { 71 | return cstart + code.replace(/\\'/g, "'").replace(/\\\\/g, "\\").replace(/[\r\t\n]/g, ' ') + ").toString().replace(/&(?!\\w+;)/g, '&').split('<').join('<').split('>').join('>').split('" + '"' + "').join('"').split(" + '"' + "'" + '"' + ").join(''').split('/').join('/'" + cend; 72 | }) 73 | .replace(c.conditionalEnd, function(match, expression) { 74 | return "';}out+='"; 75 | }) 76 | .replace(c.conditionalStart, function(match, expression) { 77 | var code = "if(" + expression + "){"; 78 | return "';" + code.replace(/\\'/g, "'").replace(/\\\\/g,"\\").replace(/[\r\t\n]/g, ' ') + "out+='"; 79 | }) 80 | .replace(c.evaluate, function(match, code) { 81 | return "';" + code.replace(/\\'/g, "'").replace(/\\\\/g,"\\").replace(/[\r\t\n]/g, ' ') + "out+='"; 82 | }) 83 | + "';return out;") 84 | .replace(/\n/g, '\\n') 85 | .replace(/\t/g, '\\t') 86 | .replace(/\r/g, '\\r') 87 | .split("out+='';").join('') 88 | .split("var out='';out+=").join('var out='); 89 | 90 | try { 91 | return new Function(c.varname, str); 92 | } catch (e) { 93 | if (typeof console !== 'undefined') console.log("Could not create a template function: " + str); 94 | throw e; 95 | } 96 | }; 97 | 98 | doT.compile = function(tmpl, def) { 99 | return doT.template(tmpl, null, def); 100 | }; 101 | }()); -------------------------------------------------------------------------------- /test/js/easytemplate.js: -------------------------------------------------------------------------------- 1 | var easyTemplate = function(s,d){ 2 | if(!s){return '';} 3 | if(s!==easyTemplate.template){ 4 | easyTemplate.template = s; 5 | easyTemplate.aStatement = easyTemplate.parsing(easyTemplate.separate(s)); 6 | } 7 | var aST = easyTemplate.aStatement; 8 | var process = function(d2){ 9 | if(d2){d = d2;} 10 | return arguments.callee; 11 | }; 12 | process.toString = function(){ 13 | return (new Function(aST[0],aST[1]))(d); 14 | }; 15 | return process; 16 | }; 17 | easyTemplate.separate = function(s){ 18 | var r = /\\'/g; 19 | var sRet = s.replace(/(<(\/?)#(.*?(?:\(.*?\))*)>)|(')|([\r\n\t])|(\$\{([^\}]*?)\})/g,function(a,b,c,d,e,f,g,h){ 20 | if(b){return '{|}'+(c?'-':'+')+d+'{|}';} 21 | if(e){return '\\\'';} 22 | if(f){return '';} 23 | if(g){return '\'+('+h.replace(r,'\'')+')+\'';} 24 | }); 25 | return sRet; 26 | }; 27 | easyTemplate.parsing = function(s){ 28 | var mName,vName,sTmp,aTmp,sFL,sEl,aList,aStm = ['var aRet = [];']; 29 | aList = s.split(/\{\|\}/); 30 | var r = /\s/; 31 | while(aList.length){ 32 | sTmp = aList.shift(); 33 | if(!sTmp){continue;} 34 | sFL = sTmp.charAt(0); 35 | if(sFL!=='+'&&sFL!=='-'){ 36 | sTmp = '\''+sTmp+'\'';aStm.push('aRet.push('+sTmp+');'); 37 | continue; 38 | } 39 | aTmp = sTmp.split(r); 40 | switch(aTmp[0]){ 41 | case '+macro':mName = aTmp[1];vName = aTmp[2];aStm.push('aRet.push(" 317 | 324 | 325 | 326 | 334 | 335 | 336 | 343 | 344 | 345 | 353 | 354 | 355 | 362 | 363 | 364 | 371 | 372 | 373 | 380 | 381 | 382 | 390 | 391 | 392 | 399 | 400 | 401 | 408 | 409 | 410 | 417 | 418 | 419 | 420 | 428 | 429 | 430 | 437 | 438 | 439 | 440 |

    引擎渲染速度测试

    441 |

    条数据 × 次渲染测试 [escape:false, cache:true]

    442 |

    建议在拥有 v8 javascript 引擎的 chrome 浏览器上进行测试,避免浏览器停止响应

    443 |

    444 |
    445 | 446 | -------------------------------------------------------------------------------- /test/test-xss.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | xss-test 6 | 7 | 8 | 9 | 10 |
    11 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 语法插件单元测试 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 155 | 156 | 157 |
    158 |
    test markup
    159 | 160 | 161 | -------------------------------------------------------------------------------- /test/tpl/index.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{title}}

    3 | 8 |
    --------------------------------------------------------------------------------